diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 821abbf02..8bd986959 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -7,7 +7,7 @@ "features": { "ghcr.io/rocker-org/devcontainer-features/apt-packages:1": { "upgradePackages": true, - "packages": "build-essential,xorg-dev,libx11-dev,libxinerama-dev,libxext-dev,mesa-common-dev,libglu1-mesa-dev,libasound2-dev,libpulse-dev,libasan8,clang-format,cppcheck,doxygen,python3,python3-pip,python3-venv,cmake,libusb-1.0-0-dev" + "packages": "build-essential,xorg-dev,libx11-dev,libxinerama-dev,libxext-dev,mesa-common-dev,libglu1-mesa-dev,libasound2-dev,libpulse-dev,libasan8,clang-format,cppcheck,doxygen,python3,python3-pip,python3-venv,cmake,libusb-1.0-0-dev,lcov" } }, "customizations": { diff --git a/.github/workflows/build-firmware-and-emulator.yml b/.github/workflows/build-firmware-and-emulator.yml index a7d0f4604..62c0d13b9 100644 --- a/.github/workflows/build-firmware-and-emulator.yml +++ b/.github/workflows/build-firmware-and-emulator.yml @@ -54,7 +54,7 @@ jobs: - name: Set up the IDF run: | - git clone -b v5.1 --recurse-submodules https://github.com/espressif/esp-idf.git ${{ runner.temp }}/esp-idf -j2 + git clone -b v5.1.1 --recurse-submodules https://github.com/espressif/esp-idf.git ${{ runner.temp }}/esp-idf -j2 ${{ runner.temp }}/esp-idf/install.ps1 - name: Compile the firmware @@ -104,7 +104,7 @@ jobs: - name: Post to a Slack channel if: (github.event_name == 'pull_request' && github.event.action == 'closed' && github.event.pull_request.merged == true) || (github.event_name == 'push' && github.ref_name == 'main') id: slack - uses: slackapi/slack-github-action@v1.23.0 + uses: slackapi/slack-github-action@v1.24.0 with: # Slack channel id, channel name, or user id to post message. # See also: https://api.slack.com/methods/chat.postMessage#channels diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml index 30c52c15f..c90d713e4 100644 --- a/.github/workflows/static.yml +++ b/.github/workflows/static.yml @@ -35,9 +35,9 @@ jobs: sudo apt update sudo apt install graphviz default-jre sudo apt remove doxygen - wget -q -P ~ https://www.doxygen.nl/files/doxygen-1.9.6.linux.bin.tar.gz - tar -xf ~/doxygen-1.9.6.linux.bin.tar.gz -C ~ - export PATH="$PATH:$HOME/doxygen-1.9.6/bin" + wget -q -P ~ https://www.doxygen.nl/files/doxygen-1.9.8.linux.bin.tar.gz + tar -xf ~/doxygen-1.9.8.linux.bin.tar.gz -C ~ + export PATH="$PATH:$HOME/doxygen-1.9.8/bin" make docs - name: Setup Pages uses: actions/configure-pages@v3 diff --git a/.gitignore b/.gitignore index 702a08ba2..3d157204e 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,10 @@ *.obj *.elf +# Coverage files +*.gcda +*.gcno + # Linker output *.ilk *.map @@ -54,6 +58,7 @@ dkms.conf build sdkconfig.old docs/html +docs/*.pu .vscode/c_cpp_properties.json .vscode/settings.json @@ -67,3 +72,12 @@ version.txt plantuml.jar swadge_emulator crash-*.txt +screenshot-*.bmp +rec-*.csv + +*.pyc +tools/rayMapEditor/autosave.rmd +perf.data +*.stackdump +coverage/ +coverage.info \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json index 153b272b0..5d8130fd2 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -9,7 +9,7 @@ "type": "cppdbg", "request": "launch", "program": "${workspaceFolder}/swadge_emulator", - "args": [], + "args": ["-t"], "stopAtEntry": false, "cwd": "${workspaceFolder}", "environment": [], diff --git a/CMakeLists.txt b/CMakeLists.txt index c9f436044..d836e272d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ # For more information about build system see -# https://docs.espressif.com/projects/esp-idf/en/v5.1/esp32s2/api-guides/build-system.html +# https://docs.espressif.com/projects/esp-idf/en/v5.1.1/esp32s2/api-guides/build-system.html # The following five lines of boilerplate have to be in your project's # CMakeLists in this exact order for cmake to work correctly cmake_minimum_required(VERSION 3.16) diff --git a/Doxyfile b/Doxyfile index a7f9fba88..5596c8f82 100644 --- a/Doxyfile +++ b/Doxyfile @@ -1,4 +1,4 @@ -# Doxyfile 1.9.6 +# Doxyfile 1.9.8 # This file describes the settings to be used by the documentation system # doxygen (www.doxygen.org) for a project. @@ -363,6 +363,17 @@ MARKDOWN_SUPPORT = YES TOC_INCLUDE_HEADINGS = 5 +# The MARKDOWN_ID_STYLE tag can be used to specify the algorithm used to +# generate identifiers for the Markdown headings. Note: Every identifier is +# unique. +# Possible values are: DOXYGEN use a fixed 'autotoc_md' string followed by a +# sequence number starting at 0 and GITHUB use the lower case version of title +# with any whitespace replaced by '-' and punctuation characters removed. +# The default value is: DOXYGEN. +# This tag requires that the tag MARKDOWN_SUPPORT is set to YES. + +MARKDOWN_ID_STYLE = GITHUB + # When enabled doxygen tries to link words that correspond to documented # classes, or namespaces to their corresponding documentation. Such a link can # be prevented in individual cases by putting a % sign in front of the word or @@ -487,6 +498,14 @@ LOOKUP_CACHE_SIZE = 0 NUM_PROC_THREADS = 0 +# If the TIMESTAMP tag is set different from NO then each generated page will +# contain the date or date and time when the page was generated. Setting this to +# NO can help when comparing the output of multiple runs. +# Possible values are: YES, NO, DATETIME and DATE. +# The default value is: NO. + +TIMESTAMP = NO + #--------------------------------------------------------------------------- # Build related configuration options #--------------------------------------------------------------------------- @@ -872,7 +891,14 @@ WARN_IF_UNDOC_ENUM_VAL = YES # a warning is encountered. If the WARN_AS_ERROR tag is set to FAIL_ON_WARNINGS # then doxygen will continue running as if WARN_AS_ERROR tag is set to NO, but # at the end of the doxygen process doxygen will return with a non-zero status. -# Possible values are: NO, YES and FAIL_ON_WARNINGS. +# If the WARN_AS_ERROR tag is set to FAIL_ON_WARNINGS_PRINT then doxygen behaves +# like FAIL_ON_WARNINGS but in case no WARN_LOGFILE is defined doxygen will not +# write the warning messages in between other messages but write them at the end +# of a run, in case a WARN_LOGFILE is defined the warning messages will be +# besides being in the defined file also be shown at the end of a run, unless +# the WARN_LOGFILE is defined as - i.e. standard output (stdout) in that case +# the behavior will remain as with the setting FAIL_ON_WARNINGS. +# Possible values are: NO, YES, FAIL_ON_WARNINGS and FAIL_ON_WARNINGS_PRINT. # The default value is: NO. WARN_AS_ERROR = NO @@ -950,18 +976,21 @@ INPUT_FILE_ENCODING = # Note the list of default checked file patterns might differ from the list of # default file extension mappings. # -# If left blank the following patterns are tested:*.c, *.cc, *.cxx, *.cpp, -# *.c++, *.java, *.ii, *.ixx, *.ipp, *.i++, *.inl, *.idl, *.ddl, *.odl, *.h, -# *.hh, *.hxx, *.hpp, *.h++, *.l, *.cs, *.d, *.php, *.php4, *.php5, *.phtml, -# *.inc, *.m, *.markdown, *.md, *.mm, *.dox (to be provided as doxygen C -# comment), *.py, *.pyw, *.f90, *.f95, *.f03, *.f08, *.f18, *.f, *.for, *.vhd, -# *.vhdl, *.ucf, *.qsf and *.ice. +# If left blank the following patterns are tested:*.c, *.cc, *.cxx, *.cxxm, +# *.cpp, *.cppm, *.c++, *.c++m, *.java, *.ii, *.ixx, *.ipp, *.i++, *.inl, *.idl, +# *.ddl, *.odl, *.h, *.hh, *.hxx, *.hpp, *.h++, *.ixx, *.l, *.cs, *.d, *.php, +# *.php4, *.php5, *.phtml, *.inc, *.m, *.markdown, *.md, *.mm, *.dox (to be +# provided as doxygen C comment), *.py, *.pyw, *.f90, *.f95, *.f03, *.f08, +# *.f18, *.f, *.for, *.vhd, *.vhdl, *.ucf, *.qsf and *.ice. FILE_PATTERNS = *.c \ *.cc \ *.cxx \ + *.cxxm \ *.cpp \ + *.cppm \ *.c++ \ + *.c++m \ *.java \ *.ii \ *.ixx \ @@ -976,6 +1005,7 @@ FILE_PATTERNS = *.c \ *.hxx \ *.hpp \ *.h++ \ + *.ixx \ *.l \ *.cs \ *.d \ @@ -1017,7 +1047,7 @@ RECURSIVE = YES # Note that relative paths are relative to the directory from which doxygen is # run. -EXCLUDE = +EXCLUDE = # The EXCLUDE_SYMLINKS tag can be used to select whether or not files or # directories that are symbolic links (a Unix file system feature) are excluded @@ -1040,9 +1070,6 @@ EXCLUDE_PATTERNS = *heatshrink* *modes* *wpa_supplicant* *crashwrap* # output. The symbol name can be a fully qualified name, a word, or if the # wildcard * is used, a substring. Examples: ANamespace, AClass, # ANamespace::AClass, ANamespace::*Test -# -# Note that the wildcards are matched against the file with absolute path, so to -# exclude all test directories use the pattern */test/* EXCLUDE_SYMBOLS = @@ -1431,15 +1458,6 @@ HTML_COLORSTYLE_SAT = 100 HTML_COLORSTYLE_GAMMA = 80 -# If the HTML_TIMESTAMP tag is set to YES then the footer of each generated HTML -# page will contain the date and time when the page was generated. Setting this -# to YES can help to show when doxygen was last run and thus if the -# documentation is up to date. -# The default value is: NO. -# This tag requires that the tag GENERATE_HTML is set to YES. - -HTML_TIMESTAMP = NO - # If the HTML_DYNAMIC_MENUS tag is set to YES then the generated HTML # documentation will contain a main index with vertical navigation menus that # are dynamically created via JavaScript. If disabled, the navigation index will @@ -1459,6 +1477,13 @@ HTML_DYNAMIC_MENUS = YES HTML_DYNAMIC_SECTIONS = NO +# If the HTML_CODE_FOLDING tag is set to YES then classes and functions can be +# dynamically folded and expanded in the generated HTML source code. +# The default value is: YES. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_CODE_FOLDING = YES + # With HTML_INDEX_NUM_ENTRIES one can control the preferred number of entries # shown in the various tree structured indices initially; the user can expand # and collapse entries dynamically later on. Doxygen will expand the tree to @@ -1589,6 +1614,16 @@ BINARY_TOC = NO TOC_EXPAND = NO +# The SITEMAP_URL tag is used to specify the full URL of the place where the +# generated documentation will be placed on the server by the user during the +# deployment of the documentation. The generated sitemap is called sitemap.xml +# and placed on the directory specified by HTML_OUTPUT. In case no SITEMAP_URL +# is specified no sitemap is generated. For information about the sitemap +# protocol see https://www.sitemaps.org +# This tag requires that the tag GENERATE_HTML is set to YES. + +SITEMAP_URL = + # If the GENERATE_QHP tag is set to YES and both QHP_NAMESPACE and # QHP_VIRTUAL_FOLDER are set, an additional index file will be generated that # can be used as input for Qt's qhelpgenerator to generate a Qt Compressed Help @@ -2077,9 +2112,16 @@ PDF_HYPERLINKS = YES USE_PDFLATEX = YES -# If the LATEX_BATCHMODE tag is set to YES, doxygen will add the \batchmode -# command to the generated LaTeX files. This will instruct LaTeX to keep running -# if errors occur, instead of asking the user for help. +# The LATEX_BATCHMODE tag signals the behavior of LaTeX in case of an error. +# Possible values are: NO same as ERROR_STOP, YES same as BATCH, BATCH In batch +# mode nothing is printed on the terminal, errors are scrolled as if is +# hit at every error; missing files that TeX tries to input or request from +# keyboard input (\read on a not open input stream) cause the job to abort, +# NON_STOP In nonstop mode the diagnostic message will appear on the terminal, +# but there is no possibility of user interaction just like in batch mode, +# SCROLL In scroll mode, TeX will stop only for missing files to input or if +# keyboard input is necessary and ERROR_STOP In errorstop mode, TeX will stop at +# each error, asking for user intervention. # The default value is: NO. # This tag requires that the tag GENERATE_LATEX is set to YES. @@ -2100,14 +2142,6 @@ LATEX_HIDE_INDICES = NO LATEX_BIB_STYLE = plain -# If the LATEX_TIMESTAMP tag is set to YES then the footer of each generated -# page will contain the date and time when the page was generated. Setting this -# to NO can help when comparing the output of multiple runs. -# The default value is: NO. -# This tag requires that the tag GENERATE_LATEX is set to YES. - -LATEX_TIMESTAMP = NO - # The LATEX_EMOJI_DIRECTORY tag is used to specify the (relative or absolute) # path from which the emoji images will be read. If a relative path is entered, # it will be relative to the LATEX_OUTPUT directory. If left blank the @@ -2273,7 +2307,7 @@ DOCBOOK_OUTPUT = docbook #--------------------------------------------------------------------------- # If the GENERATE_AUTOGEN_DEF tag is set to YES, doxygen will generate an -# AutoGen Definitions (see http://autogen.sourceforge.net/) file that captures +# AutoGen Definitions (see https://autogen.sourceforge.net/) file that captures # the structure of the code including all documentation. Note that this feature # is still experimental and incomplete at the moment. # The default value is: NO. @@ -2284,6 +2318,28 @@ GENERATE_AUTOGEN_DEF = NO # Configuration options related to Sqlite3 output #--------------------------------------------------------------------------- +# If the GENERATE_SQLITE3 tag is set to YES doxygen will generate a Sqlite3 +# database with symbols found by doxygen stored in tables. +# The default value is: NO. + +GENERATE_SQLITE3 = NO + +# The SQLITE3_OUTPUT tag is used to specify where the Sqlite3 database will be +# put. If a relative path is entered the value of OUTPUT_DIRECTORY will be put +# in front of it. +# The default directory is: sqlite3. +# This tag requires that the tag GENERATE_SQLITE3 is set to YES. + +SQLITE3_OUTPUT = sqlite3 + +# The SQLITE3_OVERWRITE_DB tag is set to YES, the existing doxygen_sqlite3.db +# database file will be recreated with each doxygen run. If set to NO, doxygen +# will warn if an a database file is already found and not modify it. +# The default value is: YES. +# This tag requires that the tag GENERATE_SQLITE3 is set to YES. + +SQLITE3_RECREATE_DB = YES + #--------------------------------------------------------------------------- # Configuration options related to the Perl module output #--------------------------------------------------------------------------- @@ -2426,15 +2482,15 @@ TAGFILES = GENERATE_TAGFILE = -# If the ALLEXTERNALS tag is set to YES, all external class will be listed in -# the class index. If set to NO, only the inherited external classes will be -# listed. +# If the ALLEXTERNALS tag is set to YES, all external classes and namespaces +# will be listed in the class and namespace index. If set to NO, only the +# inherited external classes will be listed. # The default value is: NO. ALLEXTERNALS = NO # If the EXTERNAL_GROUPS tag is set to YES, all external groups will be listed -# in the modules index. If set to NO, only the current project's groups will be +# in the topic index. If set to NO, only the current project's groups will be # listed. # The default value is: YES. @@ -2448,16 +2504,9 @@ EXTERNAL_GROUPS = YES EXTERNAL_PAGES = YES #--------------------------------------------------------------------------- -# Configuration options related to the dot tool +# Configuration options related to diagram generator tools #--------------------------------------------------------------------------- -# You can include diagrams made with dia in doxygen documentation. Doxygen will -# then run dia to produce the diagram and insert it in the documentation. The -# DIA_PATH tag allows you to specify the directory where the dia binary resides. -# If left empty dia is assumed to be found in the default search path. - -DIA_PATH = - # If set to YES the inheritance and collaboration graphs will hide inheritance # and usage relations if the target is undocumented or is not a class. # The default value is: YES. @@ -2466,7 +2515,7 @@ HIDE_UNDOC_RELATIONS = YES # If you set the HAVE_DOT tag to YES then doxygen will assume the dot tool is # available from the path. This tool is part of Graphviz (see: -# http://www.graphviz.org/), a graph visualization toolkit from AT&T and Lucent +# https://www.graphviz.org/), a graph visualization toolkit from AT&T and Lucent # Bell Labs. The other options in this section have no effect if this option is # set to NO # The default value is: NO. @@ -2519,13 +2568,15 @@ DOT_NODE_ATTR = "shape=box,height=0.2,width=0.4" DOT_FONTPATH = -# If the CLASS_GRAPH tag is set to YES (or GRAPH) then doxygen will generate a -# graph for each documented class showing the direct and indirect inheritance -# relations. In case HAVE_DOT is set as well dot will be used to draw the graph, -# otherwise the built-in generator will be used. If the CLASS_GRAPH tag is set -# to TEXT the direct and indirect inheritance relations will be shown as texts / -# links. -# Possible values are: NO, YES, TEXT and GRAPH. +# If the CLASS_GRAPH tag is set to YES or GRAPH or BUILTIN then doxygen will +# generate a graph for each documented class showing the direct and indirect +# inheritance relations. In case the CLASS_GRAPH tag is set to YES or GRAPH and +# HAVE_DOT is enabled as well, then dot will be used to draw the graph. In case +# the CLASS_GRAPH tag is set to YES and HAVE_DOT is disabled or if the +# CLASS_GRAPH tag is set to BUILTIN, then the built-in generator will be used. +# If the CLASS_GRAPH tag is set to TEXT the direct and indirect inheritance +# relations will be shown as texts / links. +# Possible values are: NO, YES, TEXT, GRAPH and BUILTIN. # The default value is: YES. CLASS_GRAPH = YES @@ -2533,15 +2584,21 @@ CLASS_GRAPH = YES # If the COLLABORATION_GRAPH tag is set to YES then doxygen will generate a # graph for each documented class showing the direct and indirect implementation # dependencies (inheritance, containment, and class references variables) of the -# class with other documented classes. +# class with other documented classes. Explicit enabling a collaboration graph, +# when COLLABORATION_GRAPH is set to NO, can be accomplished by means of the +# command \collaborationgraph. Disabling a collaboration graph can be +# accomplished by means of the command \hidecollaborationgraph. # The default value is: YES. # This tag requires that the tag HAVE_DOT is set to YES. COLLABORATION_GRAPH = YES # If the GROUP_GRAPHS tag is set to YES then doxygen will generate a graph for -# groups, showing the direct groups dependencies. See also the chapter Grouping -# in the manual. +# groups, showing the direct groups dependencies. Explicit enabling a group +# dependency graph, when GROUP_GRAPHS is set to NO, can be accomplished by means +# of the command \groupgraph. Disabling a directory graph can be accomplished by +# means of the command \hidegroupgraph. See also the chapter Grouping in the +# manual. # The default value is: YES. # This tag requires that the tag HAVE_DOT is set to YES. @@ -2601,7 +2658,9 @@ TEMPLATE_RELATIONS = NO # If the INCLUDE_GRAPH, ENABLE_PREPROCESSING and SEARCH_INCLUDES tags are set to # YES then doxygen will generate a graph for each documented file showing the # direct and indirect include dependencies of the file with other documented -# files. +# files. Explicit enabling an include graph, when INCLUDE_GRAPH is is set to NO, +# can be accomplished by means of the command \includegraph. Disabling an +# include graph can be accomplished by means of the command \hideincludegraph. # The default value is: YES. # This tag requires that the tag HAVE_DOT is set to YES. @@ -2610,7 +2669,10 @@ INCLUDE_GRAPH = YES # If the INCLUDED_BY_GRAPH, ENABLE_PREPROCESSING and SEARCH_INCLUDES tags are # set to YES then doxygen will generate a graph for each documented file showing # the direct and indirect include dependencies of the file with other documented -# files. +# files. Explicit enabling an included by graph, when INCLUDED_BY_GRAPH is set +# to NO, can be accomplished by means of the command \includedbygraph. Disabling +# an included by graph can be accomplished by means of the command +# \hideincludedbygraph. # The default value is: YES. # This tag requires that the tag HAVE_DOT is set to YES. @@ -2650,7 +2712,10 @@ GRAPHICAL_HIERARCHY = YES # If the DIRECTORY_GRAPH tag is set to YES then doxygen will show the # dependencies a directory has on other directories in a graphical way. The # dependency relations are determined by the #include relations between the -# files in the directories. +# files in the directories. Explicit enabling a directory graph, when +# DIRECTORY_GRAPH is set to NO, can be accomplished by means of the command +# \directorygraph. Disabling a directory graph can be accomplished by means of +# the command \hidedirectorygraph. # The default value is: YES. # This tag requires that the tag HAVE_DOT is set to YES. @@ -2666,7 +2731,7 @@ DIR_GRAPH_MAX_DEPTH = 1 # The DOT_IMAGE_FORMAT tag can be used to set the image format of the images # generated by dot. For an explanation of the image formats see the section # output formats in the documentation of the dot tool (Graphviz (see: -# http://www.graphviz.org/)). +# https://www.graphviz.org/)). # Note: If you choose svg you need to set HTML_FILE_EXTENSION to xhtml in order # to make the SVG files visible in IE 9+ (other browsers do not have this # requirement). @@ -2703,11 +2768,12 @@ DOT_PATH = DOTFILE_DIRS = -# The MSCFILE_DIRS tag can be used to specify one or more directories that -# contain msc files that are included in the documentation (see the \mscfile -# command). +# You can include diagrams made with dia in doxygen documentation. Doxygen will +# then run dia to produce the diagram and insert it in the documentation. The +# DIA_PATH tag allows you to specify the directory where the dia binary resides. +# If left empty dia is assumed to be found in the default search path. -MSCFILE_DIRS = +DIA_PATH = # The DIAFILE_DIRS tag can be used to specify one or more directories that # contain dia files that are included in the documentation (see the \diafile @@ -2784,3 +2850,19 @@ GENERATE_LEGEND = YES # The default value is: YES. DOT_CLEANUP = YES + +# You can define message sequence charts within doxygen comments using the \msc +# command. If the MSCGEN_TOOL tag is left empty (the default), then doxygen will +# use a built-in version of mscgen tool to produce the charts. Alternatively, +# the MSCGEN_TOOL tag can also specify the name an external tool. For instance, +# specifying prog as the value, doxygen will call the tool as prog -T +# -o . The external tool should support +# output file formats "png", "eps", "svg", and "ismap". + +MSCGEN_TOOL = + +# The MSCFILE_DIRS tag can be used to specify one or more directories that +# contain msc files that are included in the documentation (see the \mscfile +# command). + +MSCFILE_DIRS = diff --git a/assets/arrows/arrow10.png b/assets/arrows/arrow10.png new file mode 100644 index 000000000..0bf27a7ea Binary files /dev/null and b/assets/arrows/arrow10.png differ diff --git a/assets/arrows/arrow19.png b/assets/arrows/arrow19.png new file mode 100644 index 000000000..74f538a0b Binary files /dev/null and b/assets/arrows/arrow19.png differ diff --git a/assets/arrows/arrow21.png b/assets/arrows/arrow21.png new file mode 100644 index 000000000..2cdf219ac Binary files /dev/null and b/assets/arrows/arrow21.png differ diff --git a/assets/arrows/arrow5.png b/assets/arrows/arrow5.png new file mode 100644 index 000000000..e986b0400 Binary files /dev/null and b/assets/arrows/arrow5.png differ diff --git a/assets/arrows/arrow9.png b/assets/arrows/arrow9.png new file mode 100644 index 000000000..1a3127db0 Binary files /dev/null and b/assets/arrows/arrow9.png differ diff --git a/assets/breakout/levels/bombtest.bin b/assets/breakout/levels/bombtest.bin new file mode 100644 index 000000000..92f854166 Binary files /dev/null and b/assets/breakout/levels/bombtest.bin differ diff --git a/assets/breakout/levels/brkLevel1.bin b/assets/breakout/levels/brkLevel1.bin new file mode 100644 index 000000000..e89951e0e Binary files /dev/null and b/assets/breakout/levels/brkLevel1.bin differ diff --git a/assets/breakout/levels/brkLvlChar1.bin b/assets/breakout/levels/brkLvlChar1.bin new file mode 100644 index 000000000..842ed75c3 Binary files /dev/null and b/assets/breakout/levels/brkLvlChar1.bin differ diff --git a/assets/breakout/levels/intro.bin b/assets/breakout/levels/intro.bin new file mode 100644 index 000000000..94b1d4743 Binary files /dev/null and b/assets/breakout/levels/intro.bin differ diff --git a/assets/breakout/levels/leftside.bin b/assets/breakout/levels/leftside.bin new file mode 100644 index 000000000..b5e8ae296 Binary files /dev/null and b/assets/breakout/levels/leftside.bin differ diff --git a/assets/breakout/levels/mag01.bin b/assets/breakout/levels/mag01.bin new file mode 100644 index 000000000..04ae0c1f7 Binary files /dev/null and b/assets/breakout/levels/mag01.bin differ diff --git a/assets/breakout/levels/mag02.bin b/assets/breakout/levels/mag02.bin new file mode 100644 index 000000000..80821654c Binary files /dev/null and b/assets/breakout/levels/mag02.bin differ diff --git a/assets/breakout/levels/ponglike.bin b/assets/breakout/levels/ponglike.bin new file mode 100644 index 000000000..576a3a013 Binary files /dev/null and b/assets/breakout/levels/ponglike.bin differ diff --git a/assets/breakout/levels/rightside.bin b/assets/breakout/levels/rightside.bin new file mode 100644 index 000000000..17450e191 Binary files /dev/null and b/assets/breakout/levels/rightside.bin differ diff --git a/assets/breakout/levels/split.bin b/assets/breakout/levels/split.bin new file mode 100644 index 000000000..0fdb37256 Binary files /dev/null and b/assets/breakout/levels/split.bin differ diff --git a/assets/breakout/levels/starlite.bin b/assets/breakout/levels/starlite.bin new file mode 100644 index 000000000..06d9c30ba Binary files /dev/null and b/assets/breakout/levels/starlite.bin differ diff --git a/assets/breakout/levels/upsidedown.bin b/assets/breakout/levels/upsidedown.bin new file mode 100644 index 000000000..0e0a8380d Binary files /dev/null and b/assets/breakout/levels/upsidedown.bin differ diff --git a/assets/breakout/sounds/sndBounce.mid b/assets/breakout/sounds/sndBounce.mid new file mode 100644 index 000000000..c79d0d87d Binary files /dev/null and b/assets/breakout/sounds/sndBounce.mid differ diff --git a/assets/breakout/sounds/sndBreak2.mid b/assets/breakout/sounds/sndBreak2.mid new file mode 100644 index 000000000..d9d4497eb Binary files /dev/null and b/assets/breakout/sounds/sndBreak2.mid differ diff --git a/assets/breakout/sounds/sndBreak3.mid b/assets/breakout/sounds/sndBreak3.mid new file mode 100644 index 000000000..72b38e284 Binary files /dev/null and b/assets/breakout/sounds/sndBreak3.mid differ diff --git a/assets/breakout/sounds/sndBrkDie.mid b/assets/breakout/sounds/sndBrkDie.mid new file mode 100644 index 000000000..4a840c62f Binary files /dev/null and b/assets/breakout/sounds/sndBrkDie.mid differ diff --git a/assets/breakout/sounds/sndDetonate.mid b/assets/breakout/sounds/sndDetonate.mid new file mode 100644 index 000000000..fb61abd87 Binary files /dev/null and b/assets/breakout/sounds/sndDetonate.mid differ diff --git a/assets/breakout/sounds/sndDropBomb.mid b/assets/breakout/sounds/sndDropBomb.mid new file mode 100644 index 000000000..c68438886 Binary files /dev/null and b/assets/breakout/sounds/sndDropBomb.mid differ diff --git a/assets/breakout/sounds/sndTally.mid b/assets/breakout/sounds/sndTally.mid new file mode 100644 index 000000000..eb47ab549 Binary files /dev/null and b/assets/breakout/sounds/sndTally.mid differ diff --git a/assets/breakout/sounds/sndWaveBall.mid b/assets/breakout/sounds/sndWaveBall.mid new file mode 100644 index 000000000..f04eb8b26 Binary files /dev/null and b/assets/breakout/sounds/sndWaveBall.mid differ diff --git a/assets/breakout/sprites/ball.png b/assets/breakout/sprites/ball.png new file mode 100644 index 000000000..d7b86aa55 Binary files /dev/null and b/assets/breakout/sprites/ball.png differ diff --git a/assets/breakout/sprites/boom000.png b/assets/breakout/sprites/boom000.png new file mode 100644 index 000000000..ddc180fba Binary files /dev/null and b/assets/breakout/sprites/boom000.png differ diff --git a/assets/breakout/sprites/boom001.png b/assets/breakout/sprites/boom001.png new file mode 100644 index 000000000..58c8bbb47 Binary files /dev/null and b/assets/breakout/sprites/boom001.png differ diff --git a/assets/breakout/sprites/boom002.png b/assets/breakout/sprites/boom002.png new file mode 100644 index 000000000..b94a416cd Binary files /dev/null and b/assets/breakout/sprites/boom002.png differ diff --git a/assets/breakout/sprites/boom003.png b/assets/breakout/sprites/boom003.png new file mode 100644 index 000000000..92ad8000b Binary files /dev/null and b/assets/breakout/sprites/boom003.png differ diff --git a/assets/breakout/sprites/dbmb000.png b/assets/breakout/sprites/dbmb000.png new file mode 100644 index 000000000..c434cf470 Binary files /dev/null and b/assets/breakout/sprites/dbmb000.png differ diff --git a/assets/breakout/sprites/dbmb001.png b/assets/breakout/sprites/dbmb001.png new file mode 100644 index 000000000..dbf5ca35a Binary files /dev/null and b/assets/breakout/sprites/dbmb001.png differ diff --git a/assets/breakout/sprites/dbmb002.png b/assets/breakout/sprites/dbmb002.png new file mode 100644 index 000000000..cbac44341 Binary files /dev/null and b/assets/breakout/sprites/dbmb002.png differ diff --git a/assets/breakout/sprites/paddle000.png b/assets/breakout/sprites/paddle000.png new file mode 100644 index 000000000..d3a0b7adb Binary files /dev/null and b/assets/breakout/sprites/paddle000.png differ diff --git a/assets/breakout/sprites/paddle001.png b/assets/breakout/sprites/paddle001.png new file mode 100644 index 000000000..29fea9bf8 Binary files /dev/null and b/assets/breakout/sprites/paddle001.png differ diff --git a/assets/breakout/sprites/paddle002.png b/assets/breakout/sprites/paddle002.png new file mode 100644 index 000000000..aa520565b Binary files /dev/null and b/assets/breakout/sprites/paddle002.png differ diff --git a/assets/breakout/sprites/paddleVertical000.png b/assets/breakout/sprites/paddleVertical000.png new file mode 100644 index 000000000..6f977960e Binary files /dev/null and b/assets/breakout/sprites/paddleVertical000.png differ diff --git a/assets/breakout/sprites/paddleVertical001.png b/assets/breakout/sprites/paddleVertical001.png new file mode 100644 index 000000000..edd753d72 Binary files /dev/null and b/assets/breakout/sprites/paddleVertical001.png differ diff --git a/assets/breakout/sprites/paddleVertical002.png b/assets/breakout/sprites/paddleVertical002.png new file mode 100644 index 000000000..313b6b1ad Binary files /dev/null and b/assets/breakout/sprites/paddleVertical002.png differ diff --git a/assets/breakout/tiles/brkTile001.png b/assets/breakout/tiles/brkTile001.png new file mode 100644 index 000000000..075224adb Binary files /dev/null and b/assets/breakout/tiles/brkTile001.png differ diff --git a/assets/breakout/tiles/brkTile002.png b/assets/breakout/tiles/brkTile002.png new file mode 100644 index 000000000..a6588ee61 Binary files /dev/null and b/assets/breakout/tiles/brkTile002.png differ diff --git a/assets/breakout/tiles/brkTile003.png b/assets/breakout/tiles/brkTile003.png new file mode 100644 index 000000000..da7d60475 Binary files /dev/null and b/assets/breakout/tiles/brkTile003.png differ diff --git a/assets/breakout/tiles/brkTile016.png b/assets/breakout/tiles/brkTile016.png new file mode 100644 index 000000000..6ed012030 Binary files /dev/null and b/assets/breakout/tiles/brkTile016.png differ diff --git a/assets/breakout/tiles/brkTile017.png b/assets/breakout/tiles/brkTile017.png new file mode 100644 index 000000000..9a1bceffb Binary files /dev/null and b/assets/breakout/tiles/brkTile017.png differ diff --git a/assets/breakout/tiles/brkTile018.png b/assets/breakout/tiles/brkTile018.png new file mode 100644 index 000000000..43f0bc164 Binary files /dev/null and b/assets/breakout/tiles/brkTile018.png differ diff --git a/assets/breakout/tiles/brkTile019.png b/assets/breakout/tiles/brkTile019.png new file mode 100644 index 000000000..5df19ba4b Binary files /dev/null and b/assets/breakout/tiles/brkTile019.png differ diff --git a/assets/breakout/tiles/brkTile020.png b/assets/breakout/tiles/brkTile020.png new file mode 100644 index 000000000..1753b10a5 Binary files /dev/null and b/assets/breakout/tiles/brkTile020.png differ diff --git a/assets/breakout/tiles/brkTile021.png b/assets/breakout/tiles/brkTile021.png new file mode 100644 index 000000000..16202c566 Binary files /dev/null and b/assets/breakout/tiles/brkTile021.png differ diff --git a/assets/breakout/tiles/brkTile022.png b/assets/breakout/tiles/brkTile022.png new file mode 100644 index 000000000..fce1f63a7 Binary files /dev/null and b/assets/breakout/tiles/brkTile022.png differ diff --git a/assets/breakout/tiles/brkTile023.png b/assets/breakout/tiles/brkTile023.png new file mode 100644 index 000000000..67dfa7f4c Binary files /dev/null and b/assets/breakout/tiles/brkTile023.png differ diff --git a/assets/breakout/tiles/brkTile024.png b/assets/breakout/tiles/brkTile024.png new file mode 100644 index 000000000..dbd33a2f9 Binary files /dev/null and b/assets/breakout/tiles/brkTile024.png differ diff --git a/assets/breakout/tiles/brkTile025.png b/assets/breakout/tiles/brkTile025.png new file mode 100644 index 000000000..d878b40e5 Binary files /dev/null and b/assets/breakout/tiles/brkTile025.png differ diff --git a/assets/breakout/tiles/brkTile026.png b/assets/breakout/tiles/brkTile026.png new file mode 100644 index 000000000..ced102368 Binary files /dev/null and b/assets/breakout/tiles/brkTile026.png differ diff --git a/assets/breakout/tiles/brkTile027.png b/assets/breakout/tiles/brkTile027.png new file mode 100644 index 000000000..9e652583e Binary files /dev/null and b/assets/breakout/tiles/brkTile027.png differ diff --git a/assets/breakout/tiles/brkTile032.png b/assets/breakout/tiles/brkTile032.png new file mode 100644 index 000000000..9686f2174 Binary files /dev/null and b/assets/breakout/tiles/brkTile032.png differ diff --git a/assets/breakout/tiles/brkTile033.png b/assets/breakout/tiles/brkTile033.png new file mode 100644 index 000000000..0d40572f8 Binary files /dev/null and b/assets/breakout/tiles/brkTile033.png differ diff --git a/assets/breakout/tiles/brkTile034.png b/assets/breakout/tiles/brkTile034.png new file mode 100644 index 000000000..814bb8d90 Binary files /dev/null and b/assets/breakout/tiles/brkTile034.png differ diff --git a/assets/breakout/tiles/brkTile035.png b/assets/breakout/tiles/brkTile035.png new file mode 100644 index 000000000..d65c19432 Binary files /dev/null and b/assets/breakout/tiles/brkTile035.png differ diff --git a/assets/breakout/tiles/brkTile036.png b/assets/breakout/tiles/brkTile036.png new file mode 100644 index 000000000..15c1f9fd8 Binary files /dev/null and b/assets/breakout/tiles/brkTile036.png differ diff --git a/assets/breakout/tiles/brkTile037.png b/assets/breakout/tiles/brkTile037.png new file mode 100644 index 000000000..65f5c18aa Binary files /dev/null and b/assets/breakout/tiles/brkTile037.png differ diff --git a/assets/breakout/tiles/brkTile038.png b/assets/breakout/tiles/brkTile038.png new file mode 100644 index 000000000..a93e10c9c Binary files /dev/null and b/assets/breakout/tiles/brkTile038.png differ diff --git a/assets/breakout/tiles/brkTile039.png b/assets/breakout/tiles/brkTile039.png new file mode 100644 index 000000000..89e4134ef Binary files /dev/null and b/assets/breakout/tiles/brkTile039.png differ diff --git a/assets/breakout/tiles/brkTile040.png b/assets/breakout/tiles/brkTile040.png new file mode 100644 index 000000000..fd6cecc07 Binary files /dev/null and b/assets/breakout/tiles/brkTile040.png differ diff --git a/assets/breakout/tiles/brkTile041.png b/assets/breakout/tiles/brkTile041.png new file mode 100644 index 000000000..f2440bcec Binary files /dev/null and b/assets/breakout/tiles/brkTile041.png differ diff --git a/assets/breakout/tiles/brkTile042.png b/assets/breakout/tiles/brkTile042.png new file mode 100644 index 000000000..fcfa8cb99 Binary files /dev/null and b/assets/breakout/tiles/brkTile042.png differ diff --git a/assets/breakout/tiles/brkTile043.png b/assets/breakout/tiles/brkTile043.png new file mode 100644 index 000000000..7fa7e6178 Binary files /dev/null and b/assets/breakout/tiles/brkTile043.png differ diff --git a/assets/breakout/tiles/brkTile044.png b/assets/breakout/tiles/brkTile044.png new file mode 100644 index 000000000..bf0538bc8 Binary files /dev/null and b/assets/breakout/tiles/brkTile044.png differ diff --git a/assets/breakout/tiles/brkTile045.png b/assets/breakout/tiles/brkTile045.png new file mode 100644 index 000000000..7c1f0b82e Binary files /dev/null and b/assets/breakout/tiles/brkTile045.png differ diff --git a/assets/breakout/tiles/brkTile046.png b/assets/breakout/tiles/brkTile046.png new file mode 100644 index 000000000..789339201 Binary files /dev/null and b/assets/breakout/tiles/brkTile046.png differ diff --git a/assets/breakout/tiles/brkTile047.png b/assets/breakout/tiles/brkTile047.png new file mode 100644 index 000000000..f1aefdd44 Binary files /dev/null and b/assets/breakout/tiles/brkTile047.png differ diff --git a/assets/breakout/tiles/brkTile048.png b/assets/breakout/tiles/brkTile048.png new file mode 100644 index 000000000..fa98352eb Binary files /dev/null and b/assets/breakout/tiles/brkTile048.png differ diff --git a/assets/breakout/tiles/brkTile049.png b/assets/breakout/tiles/brkTile049.png new file mode 100644 index 000000000..89ee1b2b0 Binary files /dev/null and b/assets/breakout/tiles/brkTile049.png differ diff --git a/assets/breakout/tiles/brkTile050.png b/assets/breakout/tiles/brkTile050.png new file mode 100644 index 000000000..fdd31b616 Binary files /dev/null and b/assets/breakout/tiles/brkTile050.png differ diff --git a/assets/breakout/tiles/brkTile051.png b/assets/breakout/tiles/brkTile051.png new file mode 100644 index 000000000..d55509202 Binary files /dev/null and b/assets/breakout/tiles/brkTile051.png differ diff --git a/assets/breakout/tiles/brkTile052.png b/assets/breakout/tiles/brkTile052.png new file mode 100644 index 000000000..919e8f4dd Binary files /dev/null and b/assets/breakout/tiles/brkTile052.png differ diff --git a/assets/breakout/tiles/brkTile053.png b/assets/breakout/tiles/brkTile053.png new file mode 100644 index 000000000..82b3262f0 Binary files /dev/null and b/assets/breakout/tiles/brkTile053.png differ diff --git a/assets/breakout/tiles/brkTile054.png b/assets/breakout/tiles/brkTile054.png new file mode 100644 index 000000000..d463e9adc Binary files /dev/null and b/assets/breakout/tiles/brkTile054.png differ diff --git a/assets/breakout/tiles/brkTile055.png b/assets/breakout/tiles/brkTile055.png new file mode 100644 index 000000000..b23ffffff Binary files /dev/null and b/assets/breakout/tiles/brkTile055.png differ diff --git a/assets/breakout/tiles/brkTile064.png b/assets/breakout/tiles/brkTile064.png new file mode 100644 index 000000000..cc318d025 Binary files /dev/null and b/assets/breakout/tiles/brkTile064.png differ diff --git a/assets/breakout/tiles/brkTile065.png b/assets/breakout/tiles/brkTile065.png new file mode 100644 index 000000000..32b70f995 Binary files /dev/null and b/assets/breakout/tiles/brkTile065.png differ diff --git a/assets/breakout/tiles/brkTile066.png b/assets/breakout/tiles/brkTile066.png new file mode 100644 index 000000000..ab776902d Binary files /dev/null and b/assets/breakout/tiles/brkTile066.png differ diff --git a/assets/breakout/tiles/brkTile067.png b/assets/breakout/tiles/brkTile067.png new file mode 100644 index 000000000..a4bfe18bf Binary files /dev/null and b/assets/breakout/tiles/brkTile067.png differ diff --git a/assets/breakout/tiles/brkTile068.png b/assets/breakout/tiles/brkTile068.png new file mode 100644 index 000000000..f439489a6 Binary files /dev/null and b/assets/breakout/tiles/brkTile068.png differ diff --git a/assets/breakout/tiles/brkTile069.png b/assets/breakout/tiles/brkTile069.png new file mode 100644 index 000000000..06094ad6c Binary files /dev/null and b/assets/breakout/tiles/brkTile069.png differ diff --git a/assets/breakout/tiles/brkTile070.png b/assets/breakout/tiles/brkTile070.png new file mode 100644 index 000000000..85a8925aa Binary files /dev/null and b/assets/breakout/tiles/brkTile070.png differ diff --git a/assets/breakout/tiles/brkTile071.png b/assets/breakout/tiles/brkTile071.png new file mode 100644 index 000000000..3f58ae338 Binary files /dev/null and b/assets/breakout/tiles/brkTile071.png differ diff --git a/assets/breakout/tiles/brkTile072.png b/assets/breakout/tiles/brkTile072.png new file mode 100644 index 000000000..22fbc7a99 Binary files /dev/null and b/assets/breakout/tiles/brkTile072.png differ diff --git a/assets/breakout/tiles/brkTile073.png b/assets/breakout/tiles/brkTile073.png new file mode 100644 index 000000000..d565767a8 Binary files /dev/null and b/assets/breakout/tiles/brkTile073.png differ diff --git a/assets/breakout/tiles/brkTile074.png b/assets/breakout/tiles/brkTile074.png new file mode 100644 index 000000000..1793db525 Binary files /dev/null and b/assets/breakout/tiles/brkTile074.png differ diff --git a/assets/breakout/tiles/brkTile075.png b/assets/breakout/tiles/brkTile075.png new file mode 100644 index 000000000..43ba1602c Binary files /dev/null and b/assets/breakout/tiles/brkTile075.png differ diff --git a/assets/breakout/tiles/brkTile076.png b/assets/breakout/tiles/brkTile076.png new file mode 100644 index 000000000..6f875bc0d Binary files /dev/null and b/assets/breakout/tiles/brkTile076.png differ diff --git a/assets/breakout/tiles/brkTile077.png b/assets/breakout/tiles/brkTile077.png new file mode 100644 index 000000000..d3e632785 Binary files /dev/null and b/assets/breakout/tiles/brkTile077.png differ diff --git a/assets/breakout/tiles/brkTile078.png b/assets/breakout/tiles/brkTile078.png new file mode 100644 index 000000000..d19ff6436 Binary files /dev/null and b/assets/breakout/tiles/brkTile078.png differ diff --git a/assets/breakout/tiles/brkTile079.png b/assets/breakout/tiles/brkTile079.png new file mode 100644 index 000000000..74d0cf3d7 Binary files /dev/null and b/assets/breakout/tiles/brkTile079.png differ diff --git a/assets/breakout/tiles/brkTile080.png b/assets/breakout/tiles/brkTile080.png new file mode 100644 index 000000000..ede053b14 Binary files /dev/null and b/assets/breakout/tiles/brkTile080.png differ diff --git a/assets/breakout/tiles/brkTile081.png b/assets/breakout/tiles/brkTile081.png new file mode 100644 index 000000000..25ea2234d Binary files /dev/null and b/assets/breakout/tiles/brkTile081.png differ diff --git a/assets/breakout/tiles/brkTile082.png b/assets/breakout/tiles/brkTile082.png new file mode 100644 index 000000000..e287b084a Binary files /dev/null and b/assets/breakout/tiles/brkTile082.png differ diff --git a/assets/breakout/tiles/brkTile083.png b/assets/breakout/tiles/brkTile083.png new file mode 100644 index 000000000..be05d77eb Binary files /dev/null and b/assets/breakout/tiles/brkTile083.png differ diff --git a/assets/breakout/tiles/brkTile084.png b/assets/breakout/tiles/brkTile084.png new file mode 100644 index 000000000..8876ccb06 Binary files /dev/null and b/assets/breakout/tiles/brkTile084.png differ diff --git a/assets/breakout/tiles/brkTile085.png b/assets/breakout/tiles/brkTile085.png new file mode 100644 index 000000000..0f65d4a39 Binary files /dev/null and b/assets/breakout/tiles/brkTile085.png differ diff --git a/assets/breakout/tiles/brkTile086.png b/assets/breakout/tiles/brkTile086.png new file mode 100644 index 000000000..bd770754e Binary files /dev/null and b/assets/breakout/tiles/brkTile086.png differ diff --git a/assets/breakout/tiles/brkTile087.png b/assets/breakout/tiles/brkTile087.png new file mode 100644 index 000000000..d1d83957e Binary files /dev/null and b/assets/breakout/tiles/brkTile087.png differ diff --git a/assets/breakout/tiles/brkTile088.png b/assets/breakout/tiles/brkTile088.png new file mode 100644 index 000000000..cbc2466ff Binary files /dev/null and b/assets/breakout/tiles/brkTile088.png differ diff --git a/assets/breakout/tiles/brkTile089.png b/assets/breakout/tiles/brkTile089.png new file mode 100644 index 000000000..39b817b17 Binary files /dev/null and b/assets/breakout/tiles/brkTile089.png differ diff --git a/assets/breakout/tiles/brkTile090.png b/assets/breakout/tiles/brkTile090.png new file mode 100644 index 000000000..d65b1dc1c Binary files /dev/null and b/assets/breakout/tiles/brkTile090.png differ diff --git a/assets/breakout/tiles/brkTile091.png b/assets/breakout/tiles/brkTile091.png new file mode 100644 index 000000000..33cb84949 Binary files /dev/null and b/assets/breakout/tiles/brkTile091.png differ diff --git a/assets/breakout/tiles/brkTile092.png b/assets/breakout/tiles/brkTile092.png new file mode 100644 index 000000000..db07454bc Binary files /dev/null and b/assets/breakout/tiles/brkTile092.png differ diff --git a/assets/breakout/tiles/brkTile093.png b/assets/breakout/tiles/brkTile093.png new file mode 100644 index 000000000..ba741a53b Binary files /dev/null and b/assets/breakout/tiles/brkTile093.png differ diff --git a/assets/breakout/tiles/brkTile094.png b/assets/breakout/tiles/brkTile094.png new file mode 100644 index 000000000..bcbc110e9 Binary files /dev/null and b/assets/breakout/tiles/brkTile094.png differ diff --git a/assets/breakout/tiles/brkTile095.png b/assets/breakout/tiles/brkTile095.png new file mode 100644 index 000000000..c2b02a474 Binary files /dev/null and b/assets/breakout/tiles/brkTile095.png differ diff --git a/assets/breakout/tiles/brkTile096.png b/assets/breakout/tiles/brkTile096.png new file mode 100644 index 000000000..b1dd655b1 Binary files /dev/null and b/assets/breakout/tiles/brkTile096.png differ diff --git a/assets/breakout/tiles/brkTile097.png b/assets/breakout/tiles/brkTile097.png new file mode 100644 index 000000000..62c8a1b32 Binary files /dev/null and b/assets/breakout/tiles/brkTile097.png differ diff --git a/assets/breakout/tiles/brkTile098.png b/assets/breakout/tiles/brkTile098.png new file mode 100644 index 000000000..be9d3e973 Binary files /dev/null and b/assets/breakout/tiles/brkTile098.png differ diff --git a/assets/breakout/tiles/brkTile099.png b/assets/breakout/tiles/brkTile099.png new file mode 100644 index 000000000..4e52b6b20 Binary files /dev/null and b/assets/breakout/tiles/brkTile099.png differ diff --git a/assets/breakout/tiles/brkTile100.png b/assets/breakout/tiles/brkTile100.png new file mode 100644 index 000000000..9b8e1f89d Binary files /dev/null and b/assets/breakout/tiles/brkTile100.png differ diff --git a/assets/breakout/tiles/brkTile101.png b/assets/breakout/tiles/brkTile101.png new file mode 100644 index 000000000..9ae2f7b43 Binary files /dev/null and b/assets/breakout/tiles/brkTile101.png differ diff --git a/assets/breakout/tiles/brkTile102.png b/assets/breakout/tiles/brkTile102.png new file mode 100644 index 000000000..60d8aa726 Binary files /dev/null and b/assets/breakout/tiles/brkTile102.png differ diff --git a/assets/breakout/tiles/brkTile103.png b/assets/breakout/tiles/brkTile103.png new file mode 100644 index 000000000..48643bab4 Binary files /dev/null and b/assets/breakout/tiles/brkTile103.png differ diff --git a/assets/breakout/tiles/brkTile104.png b/assets/breakout/tiles/brkTile104.png new file mode 100644 index 000000000..45ca8bdb2 Binary files /dev/null and b/assets/breakout/tiles/brkTile104.png differ diff --git a/assets/breakout/tiles/brkTile105.png b/assets/breakout/tiles/brkTile105.png new file mode 100644 index 000000000..e2c70433b Binary files /dev/null and b/assets/breakout/tiles/brkTile105.png differ diff --git a/assets/breakout/tiles/brkTile106.png b/assets/breakout/tiles/brkTile106.png new file mode 100644 index 000000000..45ca8bdb2 Binary files /dev/null and b/assets/breakout/tiles/brkTile106.png differ diff --git a/assets/breakout/tiles/brkTile107.png b/assets/breakout/tiles/brkTile107.png new file mode 100644 index 000000000..e2c70433b Binary files /dev/null and b/assets/breakout/tiles/brkTile107.png differ diff --git a/assets/breakout/tiles/brkTile108.png b/assets/breakout/tiles/brkTile108.png new file mode 100644 index 000000000..45ca8bdb2 Binary files /dev/null and b/assets/breakout/tiles/brkTile108.png differ diff --git a/assets/breakout/tiles/brkTile109.png b/assets/breakout/tiles/brkTile109.png new file mode 100644 index 000000000..e2c70433b Binary files /dev/null and b/assets/breakout/tiles/brkTile109.png differ diff --git a/assets/breakout/tiles/brkTile110.png b/assets/breakout/tiles/brkTile110.png new file mode 100644 index 000000000..45ca8bdb2 Binary files /dev/null and b/assets/breakout/tiles/brkTile110.png differ diff --git a/assets/breakout/tiles/brkTile111.png b/assets/breakout/tiles/brkTile111.png new file mode 100644 index 000000000..e2c70433b Binary files /dev/null and b/assets/breakout/tiles/brkTile111.png differ diff --git a/assets/breakout/tiles/brkTile112.png b/assets/breakout/tiles/brkTile112.png new file mode 100644 index 000000000..e23e005f0 Binary files /dev/null and b/assets/breakout/tiles/brkTile112.png differ diff --git a/assets/breakout/tiles/brkTile113.png b/assets/breakout/tiles/brkTile113.png new file mode 100644 index 000000000..b7b5095b5 Binary files /dev/null and b/assets/breakout/tiles/brkTile113.png differ diff --git a/assets/breakout/tiles/brkTile114.png b/assets/breakout/tiles/brkTile114.png new file mode 100644 index 000000000..612f095e9 Binary files /dev/null and b/assets/breakout/tiles/brkTile114.png differ diff --git a/assets/breakout/tiles/brkTile115.png b/assets/breakout/tiles/brkTile115.png new file mode 100644 index 000000000..22f4d5fbf Binary files /dev/null and b/assets/breakout/tiles/brkTile115.png differ diff --git a/assets/breakout/tiles/brkTile116.png b/assets/breakout/tiles/brkTile116.png new file mode 100644 index 000000000..99ca8dc1f Binary files /dev/null and b/assets/breakout/tiles/brkTile116.png differ diff --git a/assets/breakout/tiles/brkTile117.png b/assets/breakout/tiles/brkTile117.png new file mode 100644 index 000000000..41b64df95 Binary files /dev/null and b/assets/breakout/tiles/brkTile117.png differ diff --git a/assets/breakout/tiles/brkTile118.png b/assets/breakout/tiles/brkTile118.png new file mode 100644 index 000000000..37a47472b Binary files /dev/null and b/assets/breakout/tiles/brkTile118.png differ diff --git a/assets/breakout/tiles/brkTile119.png b/assets/breakout/tiles/brkTile119.png new file mode 100644 index 000000000..8e9eade58 Binary files /dev/null and b/assets/breakout/tiles/brkTile119.png differ diff --git a/assets/breakout/tiles/brkTile120.png b/assets/breakout/tiles/brkTile120.png new file mode 100644 index 000000000..e7a62edce Binary files /dev/null and b/assets/breakout/tiles/brkTile120.png differ diff --git a/assets/breakout/tiles/brkTile121.png b/assets/breakout/tiles/brkTile121.png new file mode 100644 index 000000000..302434422 Binary files /dev/null and b/assets/breakout/tiles/brkTile121.png differ diff --git a/assets/breakout/tiles/brkTile122.png b/assets/breakout/tiles/brkTile122.png new file mode 100644 index 000000000..e7a62edce Binary files /dev/null and b/assets/breakout/tiles/brkTile122.png differ diff --git a/assets/breakout/tiles/brkTile123.png b/assets/breakout/tiles/brkTile123.png new file mode 100644 index 000000000..302434422 Binary files /dev/null and b/assets/breakout/tiles/brkTile123.png differ diff --git a/assets/breakout/tiles/brkTile124.png b/assets/breakout/tiles/brkTile124.png new file mode 100644 index 000000000..e7a62edce Binary files /dev/null and b/assets/breakout/tiles/brkTile124.png differ diff --git a/assets/breakout/tiles/brkTile125.png b/assets/breakout/tiles/brkTile125.png new file mode 100644 index 000000000..302434422 Binary files /dev/null and b/assets/breakout/tiles/brkTile125.png differ diff --git a/assets/breakout/tiles/brkTile126.png b/assets/breakout/tiles/brkTile126.png new file mode 100644 index 000000000..e7a62edce Binary files /dev/null and b/assets/breakout/tiles/brkTile126.png differ diff --git a/assets/breakout/tiles/brkTile127.png b/assets/breakout/tiles/brkTile127.png new file mode 100644 index 000000000..302434422 Binary files /dev/null and b/assets/breakout/tiles/brkTile127.png differ diff --git a/assets/fonts/radiostars.font.png b/assets/fonts/radiostars.font.png deleted file mode 100644 index 73bbf7301..000000000 Binary files a/assets/fonts/radiostars.font.png and /dev/null differ diff --git a/assets/fonts/seven_segment.font.png b/assets/fonts/seven_segment.font.png new file mode 100644 index 000000000..9e73ebaa7 Binary files /dev/null and b/assets/fonts/seven_segment.font.png differ diff --git a/assets/lumbers/alert.png b/assets/lumbers/alert.png new file mode 100644 index 000000000..5d7e2d4e7 Binary files /dev/null and b/assets/lumbers/alert.png differ diff --git a/assets/lumbers/bottom_floor1.png b/assets/lumbers/bottom_floor1.png new file mode 100644 index 000000000..b5c5e54d0 Binary files /dev/null and b/assets/lumbers/bottom_floor1.png differ diff --git a/assets/lumbers/bottom_floor10.png b/assets/lumbers/bottom_floor10.png new file mode 100644 index 000000000..912565bed Binary files /dev/null and b/assets/lumbers/bottom_floor10.png differ diff --git a/assets/lumbers/bottom_floor2.png b/assets/lumbers/bottom_floor2.png new file mode 100644 index 000000000..6cc50cf56 Binary files /dev/null and b/assets/lumbers/bottom_floor2.png differ diff --git a/assets/lumbers/bottom_floor3.png b/assets/lumbers/bottom_floor3.png new file mode 100644 index 000000000..8c374a903 Binary files /dev/null and b/assets/lumbers/bottom_floor3.png differ diff --git a/assets/lumbers/bottom_floor4.png b/assets/lumbers/bottom_floor4.png new file mode 100644 index 000000000..977d0fed9 Binary files /dev/null and b/assets/lumbers/bottom_floor4.png differ diff --git a/assets/lumbers/bottom_floor5.png b/assets/lumbers/bottom_floor5.png new file mode 100644 index 000000000..ad46a896c Binary files /dev/null and b/assets/lumbers/bottom_floor5.png differ diff --git a/assets/lumbers/bottom_floor6.png b/assets/lumbers/bottom_floor6.png new file mode 100644 index 000000000..b6816beec Binary files /dev/null and b/assets/lumbers/bottom_floor6.png differ diff --git a/assets/lumbers/bottom_floor7.png b/assets/lumbers/bottom_floor7.png new file mode 100644 index 000000000..fdfb74284 Binary files /dev/null and b/assets/lumbers/bottom_floor7.png differ diff --git a/assets/lumbers/bottom_floor8.png b/assets/lumbers/bottom_floor8.png new file mode 100644 index 000000000..3b3d39e70 Binary files /dev/null and b/assets/lumbers/bottom_floor8.png differ diff --git a/assets/lumbers/bottom_floor9.png b/assets/lumbers/bottom_floor9.png new file mode 100644 index 000000000..ca8915fe9 Binary files /dev/null and b/assets/lumbers/bottom_floor9.png differ diff --git a/assets/lumbers/enemy_a1.png b/assets/lumbers/enemy_a1.png new file mode 100644 index 000000000..5b6b34e78 Binary files /dev/null and b/assets/lumbers/enemy_a1.png differ diff --git a/assets/lumbers/enemy_a2.png b/assets/lumbers/enemy_a2.png new file mode 100644 index 000000000..e2b34eaf9 Binary files /dev/null and b/assets/lumbers/enemy_a2.png differ diff --git a/assets/lumbers/enemy_a3.png b/assets/lumbers/enemy_a3.png new file mode 100644 index 000000000..fcc1f53ad Binary files /dev/null and b/assets/lumbers/enemy_a3.png differ diff --git a/assets/lumbers/enemy_a4.png b/assets/lumbers/enemy_a4.png new file mode 100644 index 000000000..e2b34eaf9 Binary files /dev/null and b/assets/lumbers/enemy_a4.png differ diff --git a/assets/lumbers/enemy_a5.png b/assets/lumbers/enemy_a5.png new file mode 100644 index 000000000..bc9f0472f Binary files /dev/null and b/assets/lumbers/enemy_a5.png differ diff --git a/assets/lumbers/enemy_a6.png b/assets/lumbers/enemy_a6.png new file mode 100644 index 000000000..18bbedf67 Binary files /dev/null and b/assets/lumbers/enemy_a6.png differ diff --git a/assets/lumbers/enemy_a7.png b/assets/lumbers/enemy_a7.png new file mode 100644 index 000000000..e28878b7d Binary files /dev/null and b/assets/lumbers/enemy_a7.png differ diff --git a/assets/lumbers/enemy_b1.png b/assets/lumbers/enemy_b1.png new file mode 100644 index 000000000..0930f3bb8 Binary files /dev/null and b/assets/lumbers/enemy_b1.png differ diff --git a/assets/lumbers/enemy_b2.png b/assets/lumbers/enemy_b2.png new file mode 100644 index 000000000..0cb1a4b39 Binary files /dev/null and b/assets/lumbers/enemy_b2.png differ diff --git a/assets/lumbers/enemy_b3.png b/assets/lumbers/enemy_b3.png new file mode 100644 index 000000000..cbbc8adb7 Binary files /dev/null and b/assets/lumbers/enemy_b3.png differ diff --git a/assets/lumbers/enemy_b4.png b/assets/lumbers/enemy_b4.png new file mode 100644 index 000000000..0cb1a4b39 Binary files /dev/null and b/assets/lumbers/enemy_b4.png differ diff --git a/assets/lumbers/enemy_b5.png b/assets/lumbers/enemy_b5.png new file mode 100644 index 000000000..613329ea9 Binary files /dev/null and b/assets/lumbers/enemy_b5.png differ diff --git a/assets/lumbers/enemy_b6.png b/assets/lumbers/enemy_b6.png new file mode 100644 index 000000000..35096c70e Binary files /dev/null and b/assets/lumbers/enemy_b6.png differ diff --git a/assets/lumbers/enemy_b7.png b/assets/lumbers/enemy_b7.png new file mode 100644 index 000000000..baf7a8c47 Binary files /dev/null and b/assets/lumbers/enemy_b7.png differ diff --git a/assets/lumbers/enemy_c1.png b/assets/lumbers/enemy_c1.png new file mode 100644 index 000000000..52a76c183 Binary files /dev/null and b/assets/lumbers/enemy_c1.png differ diff --git a/assets/lumbers/enemy_c2.png b/assets/lumbers/enemy_c2.png new file mode 100644 index 000000000..ec84a05ba Binary files /dev/null and b/assets/lumbers/enemy_c2.png differ diff --git a/assets/lumbers/enemy_c3.png b/assets/lumbers/enemy_c3.png new file mode 100644 index 000000000..2ae1f96b6 Binary files /dev/null and b/assets/lumbers/enemy_c3.png differ diff --git a/assets/lumbers/enemy_c4.png b/assets/lumbers/enemy_c4.png new file mode 100644 index 000000000..ec84a05ba Binary files /dev/null and b/assets/lumbers/enemy_c4.png differ diff --git a/assets/lumbers/enemy_c5.png b/assets/lumbers/enemy_c5.png new file mode 100644 index 000000000..2b55a2b9a Binary files /dev/null and b/assets/lumbers/enemy_c5.png differ diff --git a/assets/lumbers/enemy_c6.png b/assets/lumbers/enemy_c6.png new file mode 100644 index 000000000..a97e66ec0 Binary files /dev/null and b/assets/lumbers/enemy_c6.png differ diff --git a/assets/lumbers/enemy_c7.png b/assets/lumbers/enemy_c7.png new file mode 100644 index 000000000..b90f146bf Binary files /dev/null and b/assets/lumbers/enemy_c7.png differ diff --git a/assets/lumbers/enemy_d1.png b/assets/lumbers/enemy_d1.png new file mode 100644 index 000000000..e029c582e Binary files /dev/null and b/assets/lumbers/enemy_d1.png differ diff --git a/assets/lumbers/enemy_d2.png b/assets/lumbers/enemy_d2.png new file mode 100644 index 000000000..adab8e808 Binary files /dev/null and b/assets/lumbers/enemy_d2.png differ diff --git a/assets/lumbers/lumbers_green_1.png b/assets/lumbers/lumbers_green_1.png new file mode 100644 index 000000000..bcb3553c4 Binary files /dev/null and b/assets/lumbers/lumbers_green_1.png differ diff --git a/assets/lumbers/lumbers_green_10.png b/assets/lumbers/lumbers_green_10.png new file mode 100644 index 000000000..e3726caf8 Binary files /dev/null and b/assets/lumbers/lumbers_green_10.png differ diff --git a/assets/lumbers/lumbers_green_11.png b/assets/lumbers/lumbers_green_11.png new file mode 100644 index 000000000..54b88f364 Binary files /dev/null and b/assets/lumbers/lumbers_green_11.png differ diff --git a/assets/lumbers/lumbers_green_12.png b/assets/lumbers/lumbers_green_12.png new file mode 100644 index 000000000..e3f6d6072 Binary files /dev/null and b/assets/lumbers/lumbers_green_12.png differ diff --git a/assets/lumbers/lumbers_green_13.png b/assets/lumbers/lumbers_green_13.png new file mode 100644 index 000000000..2a63acb53 Binary files /dev/null and b/assets/lumbers/lumbers_green_13.png differ diff --git a/assets/lumbers/lumbers_green_14.png b/assets/lumbers/lumbers_green_14.png new file mode 100644 index 000000000..095f36e18 Binary files /dev/null and b/assets/lumbers/lumbers_green_14.png differ diff --git a/assets/lumbers/lumbers_green_15.png b/assets/lumbers/lumbers_green_15.png new file mode 100644 index 000000000..32d7f562d Binary files /dev/null and b/assets/lumbers/lumbers_green_15.png differ diff --git a/assets/lumbers/lumbers_green_16.png b/assets/lumbers/lumbers_green_16.png new file mode 100644 index 000000000..95815d26b Binary files /dev/null and b/assets/lumbers/lumbers_green_16.png differ diff --git a/assets/lumbers/lumbers_green_17.png b/assets/lumbers/lumbers_green_17.png new file mode 100644 index 000000000..a8d06e167 Binary files /dev/null and b/assets/lumbers/lumbers_green_17.png differ diff --git a/assets/lumbers/lumbers_green_18.png b/assets/lumbers/lumbers_green_18.png new file mode 100644 index 000000000..89839ad4e Binary files /dev/null and b/assets/lumbers/lumbers_green_18.png differ diff --git a/assets/lumbers/lumbers_green_19.png b/assets/lumbers/lumbers_green_19.png new file mode 100644 index 000000000..8260d34f8 Binary files /dev/null and b/assets/lumbers/lumbers_green_19.png differ diff --git a/assets/lumbers/lumbers_green_2.png b/assets/lumbers/lumbers_green_2.png new file mode 100644 index 000000000..8e22b086b Binary files /dev/null and b/assets/lumbers/lumbers_green_2.png differ diff --git a/assets/lumbers/lumbers_green_20.png b/assets/lumbers/lumbers_green_20.png new file mode 100644 index 000000000..53b02abdb Binary files /dev/null and b/assets/lumbers/lumbers_green_20.png differ diff --git a/assets/lumbers/lumbers_green_21.png b/assets/lumbers/lumbers_green_21.png new file mode 100644 index 000000000..5a0edb809 Binary files /dev/null and b/assets/lumbers/lumbers_green_21.png differ diff --git a/assets/lumbers/lumbers_green_3.png b/assets/lumbers/lumbers_green_3.png new file mode 100644 index 000000000..4684f7af9 Binary files /dev/null and b/assets/lumbers/lumbers_green_3.png differ diff --git a/assets/lumbers/lumbers_green_4.png b/assets/lumbers/lumbers_green_4.png new file mode 100644 index 000000000..4684f7af9 Binary files /dev/null and b/assets/lumbers/lumbers_green_4.png differ diff --git a/assets/lumbers/lumbers_green_5.png b/assets/lumbers/lumbers_green_5.png new file mode 100644 index 000000000..c6d4cf3fa Binary files /dev/null and b/assets/lumbers/lumbers_green_5.png differ diff --git a/assets/lumbers/lumbers_green_6.png b/assets/lumbers/lumbers_green_6.png new file mode 100644 index 000000000..9f103461c Binary files /dev/null and b/assets/lumbers/lumbers_green_6.png differ diff --git a/assets/lumbers/lumbers_green_7.png b/assets/lumbers/lumbers_green_7.png new file mode 100644 index 000000000..5a77e6d15 Binary files /dev/null and b/assets/lumbers/lumbers_green_7.png differ diff --git a/assets/lumbers/lumbers_green_8.png b/assets/lumbers/lumbers_green_8.png new file mode 100644 index 000000000..7338385fa Binary files /dev/null and b/assets/lumbers/lumbers_green_8.png differ diff --git a/assets/lumbers/lumbers_green_9.png b/assets/lumbers/lumbers_green_9.png new file mode 100644 index 000000000..612991c68 Binary files /dev/null and b/assets/lumbers/lumbers_green_9.png differ diff --git a/assets/lumbers/lumbers_red_1.png b/assets/lumbers/lumbers_red_1.png new file mode 100644 index 000000000..c7e9b9abe Binary files /dev/null and b/assets/lumbers/lumbers_red_1.png differ diff --git a/assets/lumbers/lumbers_red_10.png b/assets/lumbers/lumbers_red_10.png new file mode 100644 index 000000000..e3862c46a Binary files /dev/null and b/assets/lumbers/lumbers_red_10.png differ diff --git a/assets/lumbers/lumbers_red_11.png b/assets/lumbers/lumbers_red_11.png new file mode 100644 index 000000000..f1b49259b Binary files /dev/null and b/assets/lumbers/lumbers_red_11.png differ diff --git a/assets/lumbers/lumbers_red_12.png b/assets/lumbers/lumbers_red_12.png new file mode 100644 index 000000000..27509b4a2 Binary files /dev/null and b/assets/lumbers/lumbers_red_12.png differ diff --git a/assets/lumbers/lumbers_red_13.png b/assets/lumbers/lumbers_red_13.png new file mode 100644 index 000000000..4a5af5681 Binary files /dev/null and b/assets/lumbers/lumbers_red_13.png differ diff --git a/assets/lumbers/lumbers_red_14.png b/assets/lumbers/lumbers_red_14.png new file mode 100644 index 000000000..0bb2c894a Binary files /dev/null and b/assets/lumbers/lumbers_red_14.png differ diff --git a/assets/lumbers/lumbers_red_15.png b/assets/lumbers/lumbers_red_15.png new file mode 100644 index 000000000..efe1be5e0 Binary files /dev/null and b/assets/lumbers/lumbers_red_15.png differ diff --git a/assets/lumbers/lumbers_red_16.png b/assets/lumbers/lumbers_red_16.png new file mode 100644 index 000000000..953d3ef2b Binary files /dev/null and b/assets/lumbers/lumbers_red_16.png differ diff --git a/assets/lumbers/lumbers_red_17.png b/assets/lumbers/lumbers_red_17.png new file mode 100644 index 000000000..a3f240b2e Binary files /dev/null and b/assets/lumbers/lumbers_red_17.png differ diff --git a/assets/lumbers/lumbers_red_2.png b/assets/lumbers/lumbers_red_2.png new file mode 100644 index 000000000..287a7907a Binary files /dev/null and b/assets/lumbers/lumbers_red_2.png differ diff --git a/assets/lumbers/lumbers_red_3.png b/assets/lumbers/lumbers_red_3.png new file mode 100644 index 000000000..fa7ee52e0 Binary files /dev/null and b/assets/lumbers/lumbers_red_3.png differ diff --git a/assets/lumbers/lumbers_red_4.png b/assets/lumbers/lumbers_red_4.png new file mode 100644 index 000000000..fa7ee52e0 Binary files /dev/null and b/assets/lumbers/lumbers_red_4.png differ diff --git a/assets/lumbers/lumbers_red_5.png b/assets/lumbers/lumbers_red_5.png new file mode 100644 index 000000000..428b017bf Binary files /dev/null and b/assets/lumbers/lumbers_red_5.png differ diff --git a/assets/lumbers/lumbers_red_6.png b/assets/lumbers/lumbers_red_6.png new file mode 100644 index 000000000..be6e9fbd4 Binary files /dev/null and b/assets/lumbers/lumbers_red_6.png differ diff --git a/assets/lumbers/lumbers_red_7.png b/assets/lumbers/lumbers_red_7.png new file mode 100644 index 000000000..2aa4d379d Binary files /dev/null and b/assets/lumbers/lumbers_red_7.png differ diff --git a/assets/lumbers/lumbers_red_8.png b/assets/lumbers/lumbers_red_8.png new file mode 100644 index 000000000..7645f2ff3 Binary files /dev/null and b/assets/lumbers/lumbers_red_8.png differ diff --git a/assets/lumbers/lumbers_red_9.png b/assets/lumbers/lumbers_red_9.png new file mode 100644 index 000000000..da28f55e2 Binary files /dev/null and b/assets/lumbers/lumbers_red_9.png differ diff --git a/assets/lumbers/secret_swadgeland_1.png b/assets/lumbers/secret_swadgeland_1.png new file mode 100644 index 000000000..9501a9615 Binary files /dev/null and b/assets/lumbers/secret_swadgeland_1.png differ diff --git a/assets/lumbers/secret_swadgeland_10.png b/assets/lumbers/secret_swadgeland_10.png new file mode 100644 index 000000000..e2871e397 Binary files /dev/null and b/assets/lumbers/secret_swadgeland_10.png differ diff --git a/assets/lumbers/secret_swadgeland_11.png b/assets/lumbers/secret_swadgeland_11.png new file mode 100644 index 000000000..e4e486f31 Binary files /dev/null and b/assets/lumbers/secret_swadgeland_11.png differ diff --git a/assets/lumbers/secret_swadgeland_12.png b/assets/lumbers/secret_swadgeland_12.png new file mode 100644 index 000000000..685c075db Binary files /dev/null and b/assets/lumbers/secret_swadgeland_12.png differ diff --git a/assets/lumbers/secret_swadgeland_13.png b/assets/lumbers/secret_swadgeland_13.png new file mode 100644 index 000000000..cb4d511a7 Binary files /dev/null and b/assets/lumbers/secret_swadgeland_13.png differ diff --git a/assets/lumbers/secret_swadgeland_14.png b/assets/lumbers/secret_swadgeland_14.png new file mode 100644 index 000000000..94252d713 Binary files /dev/null and b/assets/lumbers/secret_swadgeland_14.png differ diff --git a/assets/lumbers/secret_swadgeland_15.png b/assets/lumbers/secret_swadgeland_15.png new file mode 100644 index 000000000..3a623d5dc Binary files /dev/null and b/assets/lumbers/secret_swadgeland_15.png differ diff --git a/assets/lumbers/secret_swadgeland_16.png b/assets/lumbers/secret_swadgeland_16.png new file mode 100644 index 000000000..6c2aa54f3 Binary files /dev/null and b/assets/lumbers/secret_swadgeland_16.png differ diff --git a/assets/lumbers/secret_swadgeland_17.png b/assets/lumbers/secret_swadgeland_17.png new file mode 100644 index 000000000..1085cbab0 Binary files /dev/null and b/assets/lumbers/secret_swadgeland_17.png differ diff --git a/assets/lumbers/secret_swadgeland_18.png b/assets/lumbers/secret_swadgeland_18.png new file mode 100644 index 000000000..4446cda3a Binary files /dev/null and b/assets/lumbers/secret_swadgeland_18.png differ diff --git a/assets/lumbers/secret_swadgeland_19.png b/assets/lumbers/secret_swadgeland_19.png new file mode 100644 index 000000000..c6427ed01 Binary files /dev/null and b/assets/lumbers/secret_swadgeland_19.png differ diff --git a/assets/lumbers/secret_swadgeland_2.png b/assets/lumbers/secret_swadgeland_2.png new file mode 100644 index 000000000..3ac929c36 Binary files /dev/null and b/assets/lumbers/secret_swadgeland_2.png differ diff --git a/assets/lumbers/secret_swadgeland_20.png b/assets/lumbers/secret_swadgeland_20.png new file mode 100644 index 000000000..a8ca081d7 Binary files /dev/null and b/assets/lumbers/secret_swadgeland_20.png differ diff --git a/assets/lumbers/secret_swadgeland_21.png b/assets/lumbers/secret_swadgeland_21.png new file mode 100644 index 000000000..f3243941b Binary files /dev/null and b/assets/lumbers/secret_swadgeland_21.png differ diff --git a/assets/lumbers/secret_swadgeland_22.png b/assets/lumbers/secret_swadgeland_22.png new file mode 100644 index 000000000..f3243941b Binary files /dev/null and b/assets/lumbers/secret_swadgeland_22.png differ diff --git a/assets/lumbers/secret_swadgeland_3.png b/assets/lumbers/secret_swadgeland_3.png new file mode 100644 index 000000000..06a2e3196 Binary files /dev/null and b/assets/lumbers/secret_swadgeland_3.png differ diff --git a/assets/lumbers/secret_swadgeland_4.png b/assets/lumbers/secret_swadgeland_4.png new file mode 100644 index 000000000..0d9d3da8a Binary files /dev/null and b/assets/lumbers/secret_swadgeland_4.png differ diff --git a/assets/lumbers/secret_swadgeland_5.png b/assets/lumbers/secret_swadgeland_5.png new file mode 100644 index 000000000..b0c9114d1 Binary files /dev/null and b/assets/lumbers/secret_swadgeland_5.png differ diff --git a/assets/lumbers/secret_swadgeland_6.png b/assets/lumbers/secret_swadgeland_6.png new file mode 100644 index 000000000..29a5007fe Binary files /dev/null and b/assets/lumbers/secret_swadgeland_6.png differ diff --git a/assets/lumbers/secret_swadgeland_7.png b/assets/lumbers/secret_swadgeland_7.png new file mode 100644 index 000000000..7eb6ff2ac Binary files /dev/null and b/assets/lumbers/secret_swadgeland_7.png differ diff --git a/assets/lumbers/secret_swadgeland_8.png b/assets/lumbers/secret_swadgeland_8.png new file mode 100644 index 000000000..a99cee332 Binary files /dev/null and b/assets/lumbers/secret_swadgeland_8.png differ diff --git a/assets/lumbers/secret_swadgeland_9.png b/assets/lumbers/secret_swadgeland_9.png new file mode 100644 index 000000000..a7daafe0c Binary files /dev/null and b/assets/lumbers/secret_swadgeland_9.png differ diff --git a/assets/lumbers/water_floor0.png b/assets/lumbers/water_floor0.png new file mode 100644 index 000000000..393c262b3 Binary files /dev/null and b/assets/lumbers/water_floor0.png differ diff --git a/assets/lumbers/water_floor1.png b/assets/lumbers/water_floor1.png new file mode 100644 index 000000000..71386ee16 Binary files /dev/null and b/assets/lumbers/water_floor1.png differ diff --git a/assets/lumbers/water_floor2.png b/assets/lumbers/water_floor2.png new file mode 100644 index 000000000..201035ad9 Binary files /dev/null and b/assets/lumbers/water_floor2.png differ diff --git a/assets/lumbers/water_floor3.png b/assets/lumbers/water_floor3.png new file mode 100644 index 000000000..7b1be2cbc Binary files /dev/null and b/assets/lumbers/water_floor3.png differ diff --git a/assets/lumbers/water_floor4.png b/assets/lumbers/water_floor4.png new file mode 100644 index 000000000..567209fac Binary files /dev/null and b/assets/lumbers/water_floor4.png differ diff --git a/assets/lumbers/water_floor_b1.png b/assets/lumbers/water_floor_b1.png new file mode 100644 index 000000000..763f004fc Binary files /dev/null and b/assets/lumbers/water_floor_b1.png differ diff --git a/assets/lumbers/water_floor_b2.png b/assets/lumbers/water_floor_b2.png new file mode 100644 index 000000000..25a81cda6 Binary files /dev/null and b/assets/lumbers/water_floor_b2.png differ diff --git a/assets/lumbers/water_floor_b3.png b/assets/lumbers/water_floor_b3.png new file mode 100644 index 000000000..820695d77 Binary files /dev/null and b/assets/lumbers/water_floor_b3.png differ diff --git a/assets/lumbers/water_floor_b4.png b/assets/lumbers/water_floor_b4.png new file mode 100644 index 000000000..050584776 Binary files /dev/null and b/assets/lumbers/water_floor_b4.png differ diff --git a/assets/mfpaint/brush_size.png b/assets/mfpaint/brush_size.png new file mode 100644 index 000000000..d3468cfd7 Binary files /dev/null and b/assets/mfpaint/brush_size.png differ diff --git a/assets/mfpaint/circle_active.png b/assets/mfpaint/circle_active.png new file mode 100644 index 000000000..46b43f630 Binary files /dev/null and b/assets/mfpaint/circle_active.png differ diff --git a/assets/mfpaint/circle_filled_active.png b/assets/mfpaint/circle_filled_active.png new file mode 100644 index 000000000..47f8d6b74 Binary files /dev/null and b/assets/mfpaint/circle_filled_active.png differ diff --git a/assets/mfpaint/circle_filled_inactive.png b/assets/mfpaint/circle_filled_inactive.png new file mode 100644 index 000000000..9ceef1202 Binary files /dev/null and b/assets/mfpaint/circle_filled_inactive.png differ diff --git a/assets/mfpaint/circle_inactive.png b/assets/mfpaint/circle_inactive.png new file mode 100644 index 000000000..6cb765fd5 Binary files /dev/null and b/assets/mfpaint/circle_inactive.png differ diff --git a/assets/mfpaint/circle_pen_active.png b/assets/mfpaint/circle_pen_active.png new file mode 100644 index 000000000..a18ba57e8 Binary files /dev/null and b/assets/mfpaint/circle_pen_active.png differ diff --git a/assets/mfpaint/circle_pen_inactive.png b/assets/mfpaint/circle_pen_inactive.png new file mode 100644 index 000000000..ea70fcdce Binary files /dev/null and b/assets/mfpaint/circle_pen_inactive.png differ diff --git a/assets/mfpaint/curve_active.png b/assets/mfpaint/curve_active.png new file mode 100644 index 000000000..fbeaf50b7 Binary files /dev/null and b/assets/mfpaint/curve_active.png differ diff --git a/assets/mfpaint/curve_inactive.png b/assets/mfpaint/curve_inactive.png new file mode 100644 index 000000000..ad9e50e3f Binary files /dev/null and b/assets/mfpaint/curve_inactive.png differ diff --git a/assets/mfpaint/ellipse_active.png b/assets/mfpaint/ellipse_active.png new file mode 100644 index 000000000..15e0f4483 Binary files /dev/null and b/assets/mfpaint/ellipse_active.png differ diff --git a/assets/mfpaint/ellipse_inactive.png b/assets/mfpaint/ellipse_inactive.png new file mode 100644 index 000000000..af68606ec Binary files /dev/null and b/assets/mfpaint/ellipse_inactive.png differ diff --git a/assets/mfpaint/line_active.png b/assets/mfpaint/line_active.png new file mode 100644 index 000000000..7a7f56cb0 Binary files /dev/null and b/assets/mfpaint/line_active.png differ diff --git a/assets/mfpaint/line_inactive.png b/assets/mfpaint/line_inactive.png new file mode 100644 index 000000000..2bacefd8e Binary files /dev/null and b/assets/mfpaint/line_inactive.png differ diff --git a/assets/mfpaint/newfile.png b/assets/mfpaint/newfile.png new file mode 100644 index 000000000..885f6bb78 Binary files /dev/null and b/assets/mfpaint/newfile.png differ diff --git a/assets/mfpaint/overwrite.png b/assets/mfpaint/overwrite.png new file mode 100644 index 000000000..2d8485a79 Binary files /dev/null and b/assets/mfpaint/overwrite.png differ diff --git a/assets/mfpaint/paint_bucket_active.png b/assets/mfpaint/paint_bucket_active.png new file mode 100644 index 000000000..961538d98 Binary files /dev/null and b/assets/mfpaint/paint_bucket_active.png differ diff --git a/assets/mfpaint/paint_bucket_inactive.png b/assets/mfpaint/paint_bucket_inactive.png new file mode 100644 index 000000000..6c3ec8103 Binary files /dev/null and b/assets/mfpaint/paint_bucket_inactive.png differ diff --git a/assets/mfpaint/pointer.png b/assets/mfpaint/pointer.png new file mode 100644 index 000000000..efb9d0d03 Binary files /dev/null and b/assets/mfpaint/pointer.png differ diff --git a/assets/mfpaint/polygon_active.png b/assets/mfpaint/polygon_active.png new file mode 100644 index 000000000..5f848d94f Binary files /dev/null and b/assets/mfpaint/polygon_active.png differ diff --git a/assets/mfpaint/polygon_inactive.png b/assets/mfpaint/polygon_inactive.png new file mode 100644 index 000000000..a085fa074 Binary files /dev/null and b/assets/mfpaint/polygon_inactive.png differ diff --git a/assets/mfpaint/rect_active.png b/assets/mfpaint/rect_active.png new file mode 100644 index 000000000..9c018c827 Binary files /dev/null and b/assets/mfpaint/rect_active.png differ diff --git a/assets/mfpaint/rect_filled_active.png b/assets/mfpaint/rect_filled_active.png new file mode 100644 index 000000000..d3597befc Binary files /dev/null and b/assets/mfpaint/rect_filled_active.png differ diff --git a/assets/mfpaint/rect_filled_inactive.png b/assets/mfpaint/rect_filled_inactive.png new file mode 100644 index 000000000..45f27834c Binary files /dev/null and b/assets/mfpaint/rect_filled_inactive.png differ diff --git a/assets/mfpaint/rect_inactive.png b/assets/mfpaint/rect_inactive.png new file mode 100644 index 000000000..cd1589ac1 Binary files /dev/null and b/assets/mfpaint/rect_inactive.png differ diff --git a/assets/mfpaint/square_pen_active.png b/assets/mfpaint/square_pen_active.png new file mode 100644 index 000000000..0c23131ef Binary files /dev/null and b/assets/mfpaint/square_pen_active.png differ diff --git a/assets/mfpaint/square_pen_inactive.png b/assets/mfpaint/square_pen_inactive.png new file mode 100644 index 000000000..e8b7d80de Binary files /dev/null and b/assets/mfpaint/square_pen_inactive.png differ diff --git a/assets/mfpaint/squarewave_active.png b/assets/mfpaint/squarewave_active.png new file mode 100644 index 000000000..ad745af83 Binary files /dev/null and b/assets/mfpaint/squarewave_active.png differ diff --git a/assets/mfpaint/squarewave_inactive.png b/assets/mfpaint/squarewave_inactive.png new file mode 100644 index 000000000..d23b97286 Binary files /dev/null and b/assets/mfpaint/squarewave_inactive.png differ diff --git a/assets/mfpaint/toolwheel/wheel_brush.png b/assets/mfpaint/toolwheel/wheel_brush.png new file mode 100644 index 000000000..4ca1e76f8 Binary files /dev/null and b/assets/mfpaint/toolwheel/wheel_brush.png differ diff --git a/assets/mfpaint/toolwheel/wheel_color.png b/assets/mfpaint/toolwheel/wheel_color.png new file mode 100644 index 000000000..1bf98e732 Binary files /dev/null and b/assets/mfpaint/toolwheel/wheel_color.png differ diff --git a/assets/mfpaint/toolwheel/wheel_options.png b/assets/mfpaint/toolwheel/wheel_options.png new file mode 100644 index 000000000..b819d1e78 Binary files /dev/null and b/assets/mfpaint/toolwheel/wheel_options.png differ diff --git a/assets/mfpaint/toolwheel/wheel_redo.png b/assets/mfpaint/toolwheel/wheel_redo.png new file mode 100644 index 000000000..3df8912ef Binary files /dev/null and b/assets/mfpaint/toolwheel/wheel_redo.png differ diff --git a/assets/mfpaint/toolwheel/wheel_save.png b/assets/mfpaint/toolwheel/wheel_save.png new file mode 100644 index 000000000..27dab4416 Binary files /dev/null and b/assets/mfpaint/toolwheel/wheel_save.png differ diff --git a/assets/mfpaint/toolwheel/wheel_size.png b/assets/mfpaint/toolwheel/wheel_size.png new file mode 100644 index 000000000..79cc5cd40 Binary files /dev/null and b/assets/mfpaint/toolwheel/wheel_size.png differ diff --git a/assets/mfpaint/toolwheel/wheel_undo.png b/assets/mfpaint/toolwheel/wheel_undo.png new file mode 100644 index 000000000..1755d7322 Binary files /dev/null and b/assets/mfpaint/toolwheel/wheel_undo.png differ diff --git a/assets/ray/BG_CEILING.png b/assets/ray/BG_CEILING.png new file mode 100644 index 000000000..71d6bc1c4 Binary files /dev/null and b/assets/ray/BG_CEILING.png differ diff --git a/assets/ray/BG_DOOR.png b/assets/ray/BG_DOOR.png new file mode 100644 index 000000000..bfb6c033f Binary files /dev/null and b/assets/ray/BG_DOOR.png differ diff --git a/assets/ray/BG_DOOR_CHARGE.png b/assets/ray/BG_DOOR_CHARGE.png new file mode 100644 index 000000000..e0742ef53 Binary files /dev/null and b/assets/ray/BG_DOOR_CHARGE.png differ diff --git a/assets/ray/BG_DOOR_ICE.png b/assets/ray/BG_DOOR_ICE.png new file mode 100644 index 000000000..0bf576255 Binary files /dev/null and b/assets/ray/BG_DOOR_ICE.png differ diff --git a/assets/ray/BG_DOOR_MISSILE.png b/assets/ray/BG_DOOR_MISSILE.png new file mode 100644 index 000000000..8356a72f6 Binary files /dev/null and b/assets/ray/BG_DOOR_MISSILE.png differ diff --git a/assets/ray/BG_DOOR_SCRIPT.png b/assets/ray/BG_DOOR_SCRIPT.png new file mode 100644 index 000000000..ab5174adb Binary files /dev/null and b/assets/ray/BG_DOOR_SCRIPT.png differ diff --git a/assets/ray/BG_DOOR_XRAY.png b/assets/ray/BG_DOOR_XRAY.png new file mode 100644 index 000000000..1ab397251 Binary files /dev/null and b/assets/ray/BG_DOOR_XRAY.png differ diff --git a/assets/ray/BG_FLOOR.png b/assets/ray/BG_FLOOR.png new file mode 100644 index 000000000..4878169a6 Binary files /dev/null and b/assets/ray/BG_FLOOR.png differ diff --git a/assets/ray/BG_FLOOR_LAVA.png b/assets/ray/BG_FLOOR_LAVA.png new file mode 100644 index 000000000..ab9fe84b8 Binary files /dev/null and b/assets/ray/BG_FLOOR_LAVA.png differ diff --git a/assets/ray/BG_FLOOR_WATER.png b/assets/ray/BG_FLOOR_WATER.png new file mode 100644 index 000000000..d057856d2 Binary files /dev/null and b/assets/ray/BG_FLOOR_WATER.png differ diff --git a/assets/ray/BG_WALL_1.png b/assets/ray/BG_WALL_1.png new file mode 100644 index 000000000..7ea986ff8 Binary files /dev/null and b/assets/ray/BG_WALL_1.png differ diff --git a/assets/ray/BG_WALL_2.png b/assets/ray/BG_WALL_2.png new file mode 100644 index 000000000..f27cec50e Binary files /dev/null and b/assets/ray/BG_WALL_2.png differ diff --git a/assets/ray/BG_WALL_3.png b/assets/ray/BG_WALL_3.png new file mode 100644 index 000000000..26b520234 Binary files /dev/null and b/assets/ray/BG_WALL_3.png differ diff --git a/assets/ray/E_ARMORED_HURT_0.png b/assets/ray/E_ARMORED_HURT_0.png new file mode 100644 index 000000000..ad62ab9ea Binary files /dev/null and b/assets/ray/E_ARMORED_HURT_0.png differ diff --git a/assets/ray/E_ARMORED_HURT_1.png b/assets/ray/E_ARMORED_HURT_1.png new file mode 100644 index 000000000..ad62ab9ea Binary files /dev/null and b/assets/ray/E_ARMORED_HURT_1.png differ diff --git a/assets/ray/E_ARMORED_HURT_2.png b/assets/ray/E_ARMORED_HURT_2.png new file mode 100644 index 000000000..ad62ab9ea Binary files /dev/null and b/assets/ray/E_ARMORED_HURT_2.png differ diff --git a/assets/ray/E_ARMORED_HURT_3.png b/assets/ray/E_ARMORED_HURT_3.png new file mode 100644 index 000000000..ad62ab9ea Binary files /dev/null and b/assets/ray/E_ARMORED_HURT_3.png differ diff --git a/assets/ray/E_ARMORED_SHOOT_0.png b/assets/ray/E_ARMORED_SHOOT_0.png new file mode 100644 index 000000000..ad62ab9ea Binary files /dev/null and b/assets/ray/E_ARMORED_SHOOT_0.png differ diff --git a/assets/ray/E_ARMORED_SHOOT_1.png b/assets/ray/E_ARMORED_SHOOT_1.png new file mode 100644 index 000000000..ad62ab9ea Binary files /dev/null and b/assets/ray/E_ARMORED_SHOOT_1.png differ diff --git a/assets/ray/E_ARMORED_SHOOT_2.png b/assets/ray/E_ARMORED_SHOOT_2.png new file mode 100644 index 000000000..ad62ab9ea Binary files /dev/null and b/assets/ray/E_ARMORED_SHOOT_2.png differ diff --git a/assets/ray/E_ARMORED_SHOOT_3.png b/assets/ray/E_ARMORED_SHOOT_3.png new file mode 100644 index 000000000..ad62ab9ea Binary files /dev/null and b/assets/ray/E_ARMORED_SHOOT_3.png differ diff --git a/assets/ray/E_ARMORED_WALK_0.png b/assets/ray/E_ARMORED_WALK_0.png new file mode 100644 index 000000000..ad62ab9ea Binary files /dev/null and b/assets/ray/E_ARMORED_WALK_0.png differ diff --git a/assets/ray/E_ARMORED_WALK_1.png b/assets/ray/E_ARMORED_WALK_1.png new file mode 100644 index 000000000..ad62ab9ea Binary files /dev/null and b/assets/ray/E_ARMORED_WALK_1.png differ diff --git a/assets/ray/E_ARMORED_WALK_2.png b/assets/ray/E_ARMORED_WALK_2.png new file mode 100644 index 000000000..ad62ab9ea Binary files /dev/null and b/assets/ray/E_ARMORED_WALK_2.png differ diff --git a/assets/ray/E_ARMORED_WALK_3.png b/assets/ray/E_ARMORED_WALK_3.png new file mode 100644 index 000000000..ad62ab9ea Binary files /dev/null and b/assets/ray/E_ARMORED_WALK_3.png differ diff --git a/assets/ray/E_BOSS_HURT_0.png b/assets/ray/E_BOSS_HURT_0.png new file mode 100644 index 000000000..ad62ab9ea Binary files /dev/null and b/assets/ray/E_BOSS_HURT_0.png differ diff --git a/assets/ray/E_BOSS_HURT_1.png b/assets/ray/E_BOSS_HURT_1.png new file mode 100644 index 000000000..ad62ab9ea Binary files /dev/null and b/assets/ray/E_BOSS_HURT_1.png differ diff --git a/assets/ray/E_BOSS_HURT_2.png b/assets/ray/E_BOSS_HURT_2.png new file mode 100644 index 000000000..ad62ab9ea Binary files /dev/null and b/assets/ray/E_BOSS_HURT_2.png differ diff --git a/assets/ray/E_BOSS_HURT_3.png b/assets/ray/E_BOSS_HURT_3.png new file mode 100644 index 000000000..ad62ab9ea Binary files /dev/null and b/assets/ray/E_BOSS_HURT_3.png differ diff --git a/assets/ray/E_BOSS_SHOOT_0.png b/assets/ray/E_BOSS_SHOOT_0.png new file mode 100644 index 000000000..ad62ab9ea Binary files /dev/null and b/assets/ray/E_BOSS_SHOOT_0.png differ diff --git a/assets/ray/E_BOSS_SHOOT_1.png b/assets/ray/E_BOSS_SHOOT_1.png new file mode 100644 index 000000000..ad62ab9ea Binary files /dev/null and b/assets/ray/E_BOSS_SHOOT_1.png differ diff --git a/assets/ray/E_BOSS_SHOOT_2.png b/assets/ray/E_BOSS_SHOOT_2.png new file mode 100644 index 000000000..ad62ab9ea Binary files /dev/null and b/assets/ray/E_BOSS_SHOOT_2.png differ diff --git a/assets/ray/E_BOSS_SHOOT_3.png b/assets/ray/E_BOSS_SHOOT_3.png new file mode 100644 index 000000000..ad62ab9ea Binary files /dev/null and b/assets/ray/E_BOSS_SHOOT_3.png differ diff --git a/assets/ray/E_BOSS_WALK_0.png b/assets/ray/E_BOSS_WALK_0.png new file mode 100644 index 000000000..ad62ab9ea Binary files /dev/null and b/assets/ray/E_BOSS_WALK_0.png differ diff --git a/assets/ray/E_BOSS_WALK_1.png b/assets/ray/E_BOSS_WALK_1.png new file mode 100644 index 000000000..ad62ab9ea Binary files /dev/null and b/assets/ray/E_BOSS_WALK_1.png differ diff --git a/assets/ray/E_BOSS_WALK_2.png b/assets/ray/E_BOSS_WALK_2.png new file mode 100644 index 000000000..ad62ab9ea Binary files /dev/null and b/assets/ray/E_BOSS_WALK_2.png differ diff --git a/assets/ray/E_BOSS_WALK_3.png b/assets/ray/E_BOSS_WALK_3.png new file mode 100644 index 000000000..ad62ab9ea Binary files /dev/null and b/assets/ray/E_BOSS_WALK_3.png differ diff --git a/assets/ray/E_FLAMING_HURT_0.png b/assets/ray/E_FLAMING_HURT_0.png new file mode 100644 index 000000000..ad62ab9ea Binary files /dev/null and b/assets/ray/E_FLAMING_HURT_0.png differ diff --git a/assets/ray/E_FLAMING_HURT_1.png b/assets/ray/E_FLAMING_HURT_1.png new file mode 100644 index 000000000..ad62ab9ea Binary files /dev/null and b/assets/ray/E_FLAMING_HURT_1.png differ diff --git a/assets/ray/E_FLAMING_HURT_2.png b/assets/ray/E_FLAMING_HURT_2.png new file mode 100644 index 000000000..ad62ab9ea Binary files /dev/null and b/assets/ray/E_FLAMING_HURT_2.png differ diff --git a/assets/ray/E_FLAMING_HURT_3.png b/assets/ray/E_FLAMING_HURT_3.png new file mode 100644 index 000000000..ad62ab9ea Binary files /dev/null and b/assets/ray/E_FLAMING_HURT_3.png differ diff --git a/assets/ray/E_FLAMING_SHOOT_0.png b/assets/ray/E_FLAMING_SHOOT_0.png new file mode 100644 index 000000000..ad62ab9ea Binary files /dev/null and b/assets/ray/E_FLAMING_SHOOT_0.png differ diff --git a/assets/ray/E_FLAMING_SHOOT_1.png b/assets/ray/E_FLAMING_SHOOT_1.png new file mode 100644 index 000000000..ad62ab9ea Binary files /dev/null and b/assets/ray/E_FLAMING_SHOOT_1.png differ diff --git a/assets/ray/E_FLAMING_SHOOT_2.png b/assets/ray/E_FLAMING_SHOOT_2.png new file mode 100644 index 000000000..ad62ab9ea Binary files /dev/null and b/assets/ray/E_FLAMING_SHOOT_2.png differ diff --git a/assets/ray/E_FLAMING_SHOOT_3.png b/assets/ray/E_FLAMING_SHOOT_3.png new file mode 100644 index 000000000..ad62ab9ea Binary files /dev/null and b/assets/ray/E_FLAMING_SHOOT_3.png differ diff --git a/assets/ray/E_FLAMING_WALK_0.png b/assets/ray/E_FLAMING_WALK_0.png new file mode 100644 index 000000000..ad62ab9ea Binary files /dev/null and b/assets/ray/E_FLAMING_WALK_0.png differ diff --git a/assets/ray/E_FLAMING_WALK_1.png b/assets/ray/E_FLAMING_WALK_1.png new file mode 100644 index 000000000..ad62ab9ea Binary files /dev/null and b/assets/ray/E_FLAMING_WALK_1.png differ diff --git a/assets/ray/E_FLAMING_WALK_2.png b/assets/ray/E_FLAMING_WALK_2.png new file mode 100644 index 000000000..ad62ab9ea Binary files /dev/null and b/assets/ray/E_FLAMING_WALK_2.png differ diff --git a/assets/ray/E_FLAMING_WALK_3.png b/assets/ray/E_FLAMING_WALK_3.png new file mode 100644 index 000000000..ad62ab9ea Binary files /dev/null and b/assets/ray/E_FLAMING_WALK_3.png differ diff --git a/assets/ray/E_HIDDEN_HURT_0.png b/assets/ray/E_HIDDEN_HURT_0.png new file mode 100644 index 000000000..ad62ab9ea Binary files /dev/null and b/assets/ray/E_HIDDEN_HURT_0.png differ diff --git a/assets/ray/E_HIDDEN_HURT_1.png b/assets/ray/E_HIDDEN_HURT_1.png new file mode 100644 index 000000000..ad62ab9ea Binary files /dev/null and b/assets/ray/E_HIDDEN_HURT_1.png differ diff --git a/assets/ray/E_HIDDEN_HURT_2.png b/assets/ray/E_HIDDEN_HURT_2.png new file mode 100644 index 000000000..ad62ab9ea Binary files /dev/null and b/assets/ray/E_HIDDEN_HURT_2.png differ diff --git a/assets/ray/E_HIDDEN_HURT_3.png b/assets/ray/E_HIDDEN_HURT_3.png new file mode 100644 index 000000000..ad62ab9ea Binary files /dev/null and b/assets/ray/E_HIDDEN_HURT_3.png differ diff --git a/assets/ray/E_HIDDEN_SHOOT_0.png b/assets/ray/E_HIDDEN_SHOOT_0.png new file mode 100644 index 000000000..ad62ab9ea Binary files /dev/null and b/assets/ray/E_HIDDEN_SHOOT_0.png differ diff --git a/assets/ray/E_HIDDEN_SHOOT_1.png b/assets/ray/E_HIDDEN_SHOOT_1.png new file mode 100644 index 000000000..ad62ab9ea Binary files /dev/null and b/assets/ray/E_HIDDEN_SHOOT_1.png differ diff --git a/assets/ray/E_HIDDEN_SHOOT_2.png b/assets/ray/E_HIDDEN_SHOOT_2.png new file mode 100644 index 000000000..ad62ab9ea Binary files /dev/null and b/assets/ray/E_HIDDEN_SHOOT_2.png differ diff --git a/assets/ray/E_HIDDEN_SHOOT_3.png b/assets/ray/E_HIDDEN_SHOOT_3.png new file mode 100644 index 000000000..ad62ab9ea Binary files /dev/null and b/assets/ray/E_HIDDEN_SHOOT_3.png differ diff --git a/assets/ray/E_HIDDEN_WALK_0.png b/assets/ray/E_HIDDEN_WALK_0.png new file mode 100644 index 000000000..ad62ab9ea Binary files /dev/null and b/assets/ray/E_HIDDEN_WALK_0.png differ diff --git a/assets/ray/E_HIDDEN_WALK_1.png b/assets/ray/E_HIDDEN_WALK_1.png new file mode 100644 index 000000000..ad62ab9ea Binary files /dev/null and b/assets/ray/E_HIDDEN_WALK_1.png differ diff --git a/assets/ray/E_HIDDEN_WALK_2.png b/assets/ray/E_HIDDEN_WALK_2.png new file mode 100644 index 000000000..ad62ab9ea Binary files /dev/null and b/assets/ray/E_HIDDEN_WALK_2.png differ diff --git a/assets/ray/E_HIDDEN_WALK_3.png b/assets/ray/E_HIDDEN_WALK_3.png new file mode 100644 index 000000000..ad62ab9ea Binary files /dev/null and b/assets/ray/E_HIDDEN_WALK_3.png differ diff --git a/assets/ray/E_NORMAL_HURT_0.png b/assets/ray/E_NORMAL_HURT_0.png new file mode 100644 index 000000000..ad62ab9ea Binary files /dev/null and b/assets/ray/E_NORMAL_HURT_0.png differ diff --git a/assets/ray/E_NORMAL_HURT_1.png b/assets/ray/E_NORMAL_HURT_1.png new file mode 100644 index 000000000..ad62ab9ea Binary files /dev/null and b/assets/ray/E_NORMAL_HURT_1.png differ diff --git a/assets/ray/E_NORMAL_HURT_2.png b/assets/ray/E_NORMAL_HURT_2.png new file mode 100644 index 000000000..ad62ab9ea Binary files /dev/null and b/assets/ray/E_NORMAL_HURT_2.png differ diff --git a/assets/ray/E_NORMAL_HURT_3.png b/assets/ray/E_NORMAL_HURT_3.png new file mode 100644 index 000000000..ad62ab9ea Binary files /dev/null and b/assets/ray/E_NORMAL_HURT_3.png differ diff --git a/assets/ray/E_NORMAL_SHOOT_0.png b/assets/ray/E_NORMAL_SHOOT_0.png new file mode 100644 index 000000000..ad62ab9ea Binary files /dev/null and b/assets/ray/E_NORMAL_SHOOT_0.png differ diff --git a/assets/ray/E_NORMAL_SHOOT_1.png b/assets/ray/E_NORMAL_SHOOT_1.png new file mode 100644 index 000000000..ad62ab9ea Binary files /dev/null and b/assets/ray/E_NORMAL_SHOOT_1.png differ diff --git a/assets/ray/E_NORMAL_SHOOT_2.png b/assets/ray/E_NORMAL_SHOOT_2.png new file mode 100644 index 000000000..ad62ab9ea Binary files /dev/null and b/assets/ray/E_NORMAL_SHOOT_2.png differ diff --git a/assets/ray/E_NORMAL_SHOOT_3.png b/assets/ray/E_NORMAL_SHOOT_3.png new file mode 100644 index 000000000..ad62ab9ea Binary files /dev/null and b/assets/ray/E_NORMAL_SHOOT_3.png differ diff --git a/assets/ray/E_NORMAL_WALK_0.png b/assets/ray/E_NORMAL_WALK_0.png new file mode 100644 index 000000000..eda30db8f Binary files /dev/null and b/assets/ray/E_NORMAL_WALK_0.png differ diff --git a/assets/ray/E_NORMAL_WALK_1.png b/assets/ray/E_NORMAL_WALK_1.png new file mode 100644 index 000000000..5aeaf3e0f Binary files /dev/null and b/assets/ray/E_NORMAL_WALK_1.png differ diff --git a/assets/ray/E_NORMAL_WALK_2.png b/assets/ray/E_NORMAL_WALK_2.png new file mode 100644 index 000000000..03457307c Binary files /dev/null and b/assets/ray/E_NORMAL_WALK_2.png differ diff --git a/assets/ray/E_NORMAL_WALK_3.png b/assets/ray/E_NORMAL_WALK_3.png new file mode 100644 index 000000000..b7ae5d103 Binary files /dev/null and b/assets/ray/E_NORMAL_WALK_3.png differ diff --git a/assets/ray/E_STRONG_HURT_0.png b/assets/ray/E_STRONG_HURT_0.png new file mode 100644 index 000000000..ad62ab9ea Binary files /dev/null and b/assets/ray/E_STRONG_HURT_0.png differ diff --git a/assets/ray/E_STRONG_HURT_1.png b/assets/ray/E_STRONG_HURT_1.png new file mode 100644 index 000000000..ad62ab9ea Binary files /dev/null and b/assets/ray/E_STRONG_HURT_1.png differ diff --git a/assets/ray/E_STRONG_HURT_2.png b/assets/ray/E_STRONG_HURT_2.png new file mode 100644 index 000000000..ad62ab9ea Binary files /dev/null and b/assets/ray/E_STRONG_HURT_2.png differ diff --git a/assets/ray/E_STRONG_HURT_3.png b/assets/ray/E_STRONG_HURT_3.png new file mode 100644 index 000000000..ad62ab9ea Binary files /dev/null and b/assets/ray/E_STRONG_HURT_3.png differ diff --git a/assets/ray/E_STRONG_SHOOT_0.png b/assets/ray/E_STRONG_SHOOT_0.png new file mode 100644 index 000000000..ad62ab9ea Binary files /dev/null and b/assets/ray/E_STRONG_SHOOT_0.png differ diff --git a/assets/ray/E_STRONG_SHOOT_1.png b/assets/ray/E_STRONG_SHOOT_1.png new file mode 100644 index 000000000..ad62ab9ea Binary files /dev/null and b/assets/ray/E_STRONG_SHOOT_1.png differ diff --git a/assets/ray/E_STRONG_SHOOT_2.png b/assets/ray/E_STRONG_SHOOT_2.png new file mode 100644 index 000000000..ad62ab9ea Binary files /dev/null and b/assets/ray/E_STRONG_SHOOT_2.png differ diff --git a/assets/ray/E_STRONG_SHOOT_3.png b/assets/ray/E_STRONG_SHOOT_3.png new file mode 100644 index 000000000..ad62ab9ea Binary files /dev/null and b/assets/ray/E_STRONG_SHOOT_3.png differ diff --git a/assets/ray/E_STRONG_WALK_0.png b/assets/ray/E_STRONG_WALK_0.png new file mode 100644 index 000000000..ad62ab9ea Binary files /dev/null and b/assets/ray/E_STRONG_WALK_0.png differ diff --git a/assets/ray/E_STRONG_WALK_1.png b/assets/ray/E_STRONG_WALK_1.png new file mode 100644 index 000000000..ad62ab9ea Binary files /dev/null and b/assets/ray/E_STRONG_WALK_1.png differ diff --git a/assets/ray/E_STRONG_WALK_2.png b/assets/ray/E_STRONG_WALK_2.png new file mode 100644 index 000000000..ad62ab9ea Binary files /dev/null and b/assets/ray/E_STRONG_WALK_2.png differ diff --git a/assets/ray/E_STRONG_WALK_3.png b/assets/ray/E_STRONG_WALK_3.png new file mode 100644 index 000000000..ad62ab9ea Binary files /dev/null and b/assets/ray/E_STRONG_WALK_3.png differ diff --git a/assets/ray/GUN_ICE.png b/assets/ray/GUN_ICE.png new file mode 100644 index 000000000..76f98df1d Binary files /dev/null and b/assets/ray/GUN_ICE.png differ diff --git a/assets/ray/GUN_MISSILE.png b/assets/ray/GUN_MISSILE.png new file mode 100644 index 000000000..0048bb0f4 Binary files /dev/null and b/assets/ray/GUN_MISSILE.png differ diff --git a/assets/ray/GUN_NORMAL.png b/assets/ray/GUN_NORMAL.png new file mode 100644 index 000000000..34d66e758 Binary files /dev/null and b/assets/ray/GUN_NORMAL.png differ diff --git a/assets/ray/GUN_XRAY.png b/assets/ray/GUN_XRAY.png new file mode 100644 index 000000000..6cb5c6d33 Binary files /dev/null and b/assets/ray/GUN_XRAY.png differ diff --git a/assets/ray/OBJ_BULLET_CHARGE.png b/assets/ray/OBJ_BULLET_CHARGE.png new file mode 100644 index 000000000..ecfc9e712 Binary files /dev/null and b/assets/ray/OBJ_BULLET_CHARGE.png differ diff --git a/assets/ray/OBJ_BULLET_ICE.png b/assets/ray/OBJ_BULLET_ICE.png new file mode 100644 index 000000000..567cc0425 Binary files /dev/null and b/assets/ray/OBJ_BULLET_ICE.png differ diff --git a/assets/ray/OBJ_BULLET_MISSILE.png b/assets/ray/OBJ_BULLET_MISSILE.png new file mode 100644 index 000000000..8115b6fba Binary files /dev/null and b/assets/ray/OBJ_BULLET_MISSILE.png differ diff --git a/assets/ray/OBJ_BULLET_NORMAL.png b/assets/ray/OBJ_BULLET_NORMAL.png new file mode 100644 index 000000000..d4e807895 Binary files /dev/null and b/assets/ray/OBJ_BULLET_NORMAL.png differ diff --git a/assets/ray/OBJ_BULLET_XRAY.png b/assets/ray/OBJ_BULLET_XRAY.png new file mode 100644 index 000000000..c8b9294d0 Binary files /dev/null and b/assets/ray/OBJ_BULLET_XRAY.png differ diff --git a/assets/ray/OBJ_ITEM_ARTIFACT.png b/assets/ray/OBJ_ITEM_ARTIFACT.png new file mode 100644 index 000000000..6d5630eca Binary files /dev/null and b/assets/ray/OBJ_ITEM_ARTIFACT.png differ diff --git a/assets/ray/OBJ_ITEM_BEAM.png b/assets/ray/OBJ_ITEM_BEAM.png new file mode 100644 index 000000000..9f919f7a6 Binary files /dev/null and b/assets/ray/OBJ_ITEM_BEAM.png differ diff --git a/assets/ray/OBJ_ITEM_CHARGE_BEAM.png b/assets/ray/OBJ_ITEM_CHARGE_BEAM.png new file mode 100644 index 000000000..66b75495a Binary files /dev/null and b/assets/ray/OBJ_ITEM_CHARGE_BEAM.png differ diff --git a/assets/ray/OBJ_ITEM_ENERGY_TANK.png b/assets/ray/OBJ_ITEM_ENERGY_TANK.png new file mode 100644 index 000000000..a16e04dca Binary files /dev/null and b/assets/ray/OBJ_ITEM_ENERGY_TANK.png differ diff --git a/assets/ray/OBJ_ITEM_ICE.png b/assets/ray/OBJ_ITEM_ICE.png new file mode 100644 index 000000000..9d7977e8d Binary files /dev/null and b/assets/ray/OBJ_ITEM_ICE.png differ diff --git a/assets/ray/OBJ_ITEM_KEY.png b/assets/ray/OBJ_ITEM_KEY.png new file mode 100644 index 000000000..1faf1f2f6 Binary files /dev/null and b/assets/ray/OBJ_ITEM_KEY.png differ diff --git a/assets/ray/OBJ_ITEM_MISSILE.png b/assets/ray/OBJ_ITEM_MISSILE.png new file mode 100644 index 000000000..b04a0ab95 Binary files /dev/null and b/assets/ray/OBJ_ITEM_MISSILE.png differ diff --git a/assets/ray/OBJ_ITEM_PICKUP_ENERGY.png b/assets/ray/OBJ_ITEM_PICKUP_ENERGY.png new file mode 100644 index 000000000..d733b5c9e Binary files /dev/null and b/assets/ray/OBJ_ITEM_PICKUP_ENERGY.png differ diff --git a/assets/ray/OBJ_ITEM_PICKUP_MISSILE.png b/assets/ray/OBJ_ITEM_PICKUP_MISSILE.png new file mode 100644 index 000000000..90fea127f Binary files /dev/null and b/assets/ray/OBJ_ITEM_PICKUP_MISSILE.png differ diff --git a/assets/ray/OBJ_ITEM_SUIT_LAVA.png b/assets/ray/OBJ_ITEM_SUIT_LAVA.png new file mode 100644 index 000000000..40077e6c9 Binary files /dev/null and b/assets/ray/OBJ_ITEM_SUIT_LAVA.png differ diff --git a/assets/ray/OBJ_ITEM_SUIT_WATER.png b/assets/ray/OBJ_ITEM_SUIT_WATER.png new file mode 100644 index 000000000..5c286df58 Binary files /dev/null and b/assets/ray/OBJ_ITEM_SUIT_WATER.png differ diff --git a/assets/ray/OBJ_ITEM_XRAY.png b/assets/ray/OBJ_ITEM_XRAY.png new file mode 100644 index 000000000..84efd8d7a Binary files /dev/null and b/assets/ray/OBJ_ITEM_XRAY.png differ diff --git a/assets/ray/OBJ_SCENERY_TERMINAL.png b/assets/ray/OBJ_SCENERY_TERMINAL.png new file mode 100644 index 000000000..abce26fc0 Binary files /dev/null and b/assets/ray/OBJ_SCENERY_TERMINAL.png differ diff --git a/assets/ray/demo.rmd b/assets/ray/demo.rmd new file mode 100644 index 000000000..e85ad3148 Binary files /dev/null and b/assets/ray/demo.rmd differ diff --git a/assets/soko/legacy_puzzles/sk_overworld1.png b/assets/soko/legacy_puzzles/sk_overworld1.png new file mode 100644 index 000000000..19d69c359 Binary files /dev/null and b/assets/soko/legacy_puzzles/sk_overworld1.png differ diff --git a/assets/soko/legacy_puzzles/sk_sticky_test.png b/assets/soko/legacy_puzzles/sk_sticky_test.png new file mode 100644 index 000000000..4315a97b9 Binary files /dev/null and b/assets/soko/legacy_puzzles/sk_sticky_test.png differ diff --git a/assets/soko/legacy_puzzles/sk_test1.png b/assets/soko/legacy_puzzles/sk_test1.png new file mode 100644 index 000000000..d5fe56686 Binary files /dev/null and b/assets/soko/legacy_puzzles/sk_test1.png differ diff --git a/assets/soko/legacy_puzzles/sk_test2.png b/assets/soko/legacy_puzzles/sk_test2.png new file mode 100644 index 000000000..999561134 Binary files /dev/null and b/assets/soko/legacy_puzzles/sk_test2.png differ diff --git a/assets/soko/legacy_puzzles/sk_test3.png b/assets/soko/legacy_puzzles/sk_test3.png new file mode 100644 index 000000000..54fa644d0 Binary files /dev/null and b/assets/soko/legacy_puzzles/sk_test3.png differ diff --git a/assets/soko/legacy_puzzles/sk_testpuzzle.png b/assets/soko/legacy_puzzles/sk_testpuzzle.png new file mode 100644 index 000000000..5480ffc24 Binary files /dev/null and b/assets/soko/legacy_puzzles/sk_testpuzzle.png differ diff --git a/assets/soko/puzzles/SK_LEVEL_LIST.txt b/assets/soko/puzzles/SK_LEVEL_LIST.txt new file mode 100644 index 000000000..7edf989f3 --- /dev/null +++ b/assets/soko/puzzles/SK_LEVEL_LIST.txt @@ -0,0 +1,7 @@ +1:sk_binOverworld.bin: +9:sk_warehouse.bin: +3:sk_blargablarg.bin: +7:sk_stinky.bin: +5:sk_lump.bin: +12:sk_blubBlub.bin: +77:sk_7777testest.bin: \ No newline at end of file diff --git a/assets/soko/puzzles/sk_binOverworld.bin b/assets/soko/puzzles/sk_binOverworld.bin new file mode 100644 index 000000000..9aab206e2 Binary files /dev/null and b/assets/soko/puzzles/sk_binOverworld.bin differ diff --git a/assets/soko/puzzles/sk_warehouse.bin b/assets/soko/puzzles/sk_warehouse.bin new file mode 100644 index 000000000..216b97486 Binary files /dev/null and b/assets/soko/puzzles/sk_warehouse.bin differ diff --git a/assets/soko/sk_crate.png b/assets/soko/sk_crate.png new file mode 100644 index 000000000..6f3b727cd Binary files /dev/null and b/assets/soko/sk_crate.png differ diff --git a/assets/soko/sk_player.png b/assets/soko/sk_player.png new file mode 100644 index 000000000..f22dd4ca1 Binary files /dev/null and b/assets/soko/sk_player.png differ diff --git a/assets/soko/sk_player_down.png b/assets/soko/sk_player_down.png new file mode 100644 index 000000000..dbde5b32e Binary files /dev/null and b/assets/soko/sk_player_down.png differ diff --git a/assets/soko/sk_player_left.png b/assets/soko/sk_player_left.png new file mode 100644 index 000000000..a52036ed7 Binary files /dev/null and b/assets/soko/sk_player_left.png differ diff --git a/assets/soko/sk_player_right.png b/assets/soko/sk_player_right.png new file mode 100644 index 000000000..be378a7f4 Binary files /dev/null and b/assets/soko/sk_player_right.png differ diff --git a/assets/soko/sk_player_up.png b/assets/soko/sk_player_up.png new file mode 100644 index 000000000..2ad084b76 Binary files /dev/null and b/assets/soko/sk_player_up.png differ diff --git a/assets/soko/sk_sticky_crate.png b/assets/soko/sk_sticky_crate.png new file mode 100644 index 000000000..c1d430725 Binary files /dev/null and b/assets/soko/sk_sticky_crate.png differ diff --git a/components/hdw-battmon/include/hdw-battmon.h b/components/hdw-battmon/include/hdw-battmon.h index ec33782ed..009b57842 100644 --- a/components/hdw-battmon/include/hdw-battmon.h +++ b/components/hdw-battmon/include/hdw-battmon.h @@ -3,11 +3,11 @@ * \section battmon_design Design Philosophy * * The battery monitor uses the Analog + * href="https://docs.espressif.com/projects/esp-idf/en/v5.1.1/esp32s2/api-reference/peripherals/adc_oneshot.html">Analog * to Digital Converter (ADC) Oneshot Mode Driver. * * The battery monitor code is based on the ADC Single Read + * href="https://github.com/espressif/esp-idf/tree/v5.1.1/examples/peripherals/adc/oneshot_read">ADC Single Read * Example. * * \warning The battery monitor and microphone (hdw-mic.h) cannot be used at the same time! Each mode can either diff --git a/components/hdw-btn/include/hdw-btn.h b/components/hdw-btn/include/hdw-btn.h index 74e4f465a..cec51ffc2 100644 --- a/components/hdw-btn/include/hdw-btn.h +++ b/components/hdw-btn/include/hdw-btn.h @@ -23,7 +23,7 @@ * to be received by the Swadge mode. * * The push-button GPIOs are all read at the same time using Dedicated + * href="https://docs.espressif.com/projects/esp-idf/en/v5.1.1/esp32s2/api-reference/peripherals/dedic_gpio.html">Dedicated * GPIO. * * Originally the push-buttons would trigger an interrupt, but we found that to have less reliable results with more @@ -43,7 +43,7 @@ * well as the intensity of the touch. * * Touch-pad areas are set up and read with Touch + * href="https://docs.espressif.com/projects/esp-idf/en/v5.1.1/esp32s2/api-reference/peripherals/touch_pad.html">Touch * Sensor. * * \section btn_usage Usage diff --git a/components/hdw-bzr/include/hdw-bzr.h b/components/hdw-bzr/include/hdw-bzr.h index 9db982e23..ea9e20bba 100644 --- a/components/hdw-bzr/include/hdw-bzr.h +++ b/components/hdw-bzr/include/hdw-bzr.h @@ -3,7 +3,7 @@ * \section bzr_design Design Philosophy * * The buzzers are driven by the LEDC + * href="https://docs.espressif.com/projects/esp-idf/en/v5.1.1/esp32s2/api-reference/peripherals/ledc.html">LEDC * peripheral. This is usually used to generate a PWM signal to control the intensity of an LED, but here it * generates frequencies for the buzzers. * diff --git a/components/hdw-esp-now/hdw-esp-now.c b/components/hdw-esp-now/hdw-esp-now.c index 8e397a95c..94ff9c633 100644 --- a/components/hdw-esp-now/hdw-esp-now.c +++ b/components/hdw-esp-now/hdw-esp-now.c @@ -183,7 +183,7 @@ esp_err_t initEspNow(hostEspNowRecvCb_t recvCb, hostEspNowSendCb_t sendCb, gpio_ .reserved = 0 /* Reserved for future feature set */ }, }; - if (ESP_OK != (err = esp_wifi_set_config(WIFI_IF_NAN, &config))) + if (ESP_OK != (err = esp_wifi_set_config(WIFI_IF_STA, &config))) { ESP_LOGW("ESPNOW", "Couldn't set station config"); return err; @@ -202,7 +202,7 @@ esp_err_t initEspNow(hostEspNowRecvCb_t recvCb, hostEspNowSendCb_t sendCb, gpio_ return err; } - if (ESP_OK != (err = esp_wifi_config_80211_tx_rate(WIFI_IF_NAN, WIFI_RATE))) + if (ESP_OK != (err = esp_wifi_config_80211_tx_rate(WIFI_IF_STA, WIFI_RATE))) { ESP_LOGW("ESPNOW", "Couldn't set PHY rate %s", esp_err_to_name(err)); return err; @@ -214,7 +214,7 @@ esp_err_t initEspNow(hostEspNowRecvCb_t recvCb, hostEspNowSendCb_t sendCb, gpio_ return err; } - if (ESP_OK != (err = esp_wifi_config_espnow_rate(WIFI_IF_NAN, WIFI_RATE))) + if (ESP_OK != (err = esp_wifi_config_espnow_rate(WIFI_IF_STA, WIFI_RATE))) { ESP_LOGW("ESPNOW", "Couldn't set PHY rate %s", esp_err_to_name(err)); return err; @@ -228,7 +228,7 @@ esp_err_t initEspNow(hostEspNowRecvCb_t recvCb, hostEspNowSendCb_t sendCb, gpio_ } // Set data rate - if (ESP_OK != (err = esp_wifi_internal_set_fix_rate(WIFI_IF_NAN, true, WIFI_RATE))) + if (ESP_OK != (err = esp_wifi_internal_set_fix_rate(WIFI_IF_STA, true, WIFI_RATE))) { ESP_LOGW("ESPNOW", "Couldn't set data rate"); return err; @@ -289,7 +289,7 @@ esp_err_t espNowUseWireless(void) .peer_addr = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}, .lmk = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, .channel = ESPNOW_CHANNEL, - .ifidx = WIFI_IF_NAN, + .ifidx = WIFI_IF_STA, .encrypt = 0, .priv = NULL}; if (ESP_OK != (err = esp_now_add_peer(&broadcastPeer))) diff --git a/components/hdw-esp-now/include/hdw-esp-now.h b/components/hdw-esp-now/include/hdw-esp-now.h index d08a86bdd..d5f4db862 100644 --- a/components/hdw-esp-now/include/hdw-esp-now.h +++ b/components/hdw-esp-now/include/hdw-esp-now.h @@ -5,7 +5,7 @@ * \section esp-now_design Design Philosophy * * ESP-NOW + * href="https://docs.espressif.com/projects/esp-idf/en/v5.1.1/esp32s2/api-reference/network/esp_now.html">ESP-NOW * is a kind of connection-less Wi-Fi communication protocol that is defined by Espressif. This component manages * ESP-NOW so that you don't have to. It provides a simple wrapper to broadcast a packet, espNowSend(), and passes all * received packets through a callback given to initEspNow(). @@ -16,7 +16,7 @@ * This pairing can be done using p2pConnection.c. * * This component can also facilitate communication between to Swadges using a Universal + * href="https://docs.espressif.com/projects/esp-idf/en/v5.1.1/esp32s2/api-reference/peripherals/uart.html">Universal * Asynchronous Receiver/Transmitter (UART) wired connection. The wired UART is significantly faster and * significantly more reliable than the wireless one, but it does require a physical wire. * diff --git a/components/hdw-led/include/hdw-led.h b/components/hdw-led/include/hdw-led.h index 5cce04074..ea14a7436 100644 --- a/components/hdw-led/include/hdw-led.h +++ b/components/hdw-led/include/hdw-led.h @@ -3,7 +3,7 @@ * \section led_design Design Philosophy * * LED code is based on Espressif's RMT + * href="https://github.com/espressif/esp-idf/tree/v5.1.1/examples/peripherals/rmt/led_strip">Espressif's RMT * Transmit Example - LED Strip. * * Each LED has a red, green, and blue component. Each component ranges from 0 to 255. diff --git a/components/hdw-mic/include/hdw-mic.h b/components/hdw-mic/include/hdw-mic.h index 554738839..029e06583 100644 --- a/components/hdw-mic/include/hdw-mic.h +++ b/components/hdw-mic/include/hdw-mic.h @@ -3,11 +3,11 @@ * \section mic_design Design Philosophy * * The microphone uses the Analog + * href="https://docs.espressif.com/projects/esp-idf/en/v5.1.1/esp32s2/api-reference/peripherals/adc_continuous.html">Analog * to Digital Converter (ADC) Continuous Mode Driver. * * The microphone code is based on the ADC DMA + * href="https://github.com/espressif/esp-idf/tree/v5.1.1/examples/peripherals/adc/continuous_read">ADC DMA * Example. * * The microphone is continuously sampled at 8KHz. diff --git a/components/hdw-nvs/include/hdw-nvs.h b/components/hdw-nvs/include/hdw-nvs.h index 7294f6bf7..ef06fd823 100644 --- a/components/hdw-nvs/include/hdw-nvs.h +++ b/components/hdw-nvs/include/hdw-nvs.h @@ -3,7 +3,7 @@ * \section nvs_design Design Philosophy * * The hdw-nvs component is a convenience wrapper for the IDF's Non-volatile + * href="https://docs.espressif.com/projects/esp-idf/en/v5.1.1/esp32s2/api-reference/storage/nvs_flash.html">Non-volatile * Storage Library. Non-volatile storage (NVS) library is designed to store key-value pairs in flash. * * The goal is to make reading and writing integer and blob values easy and simple for Swadge modes. diff --git a/components/hdw-spiffs/include/hdw-spiffs.h b/components/hdw-spiffs/include/hdw-spiffs.h index 46e3a3f53..05f43258c 100644 --- a/components/hdw-spiffs/include/hdw-spiffs.h +++ b/components/hdw-spiffs/include/hdw-spiffs.h @@ -4,7 +4,7 @@ * * SPIFFS is a file system intended for SPI NOR flash devices on embedded targets. It supports wear levelling, file * system consistency checks, and more. The full API reference can be found here: SPIFFS + * href="https://docs.espressif.com/projects/esp-idf/en/v5.1.1/esp32s2/api-reference/storage/spiffs.html">SPIFFS * Filesystem. * * Ths Swadge treats SPIFFS as a read-only file system. diff --git a/components/hdw-temperature/include/hdw-temperature.h b/components/hdw-temperature/include/hdw-temperature.h index eb19ada26..f8182573e 100644 --- a/components/hdw-temperature/include/hdw-temperature.h +++ b/components/hdw-temperature/include/hdw-temperature.h @@ -3,7 +3,7 @@ * \section temperature_design Design Philosophy * * Temperature sensor code code is based on Temperature Sensor + * href="https://github.com/espressif/esp-idf/tree/v5.1.1/examples/peripherals/temp_sensor">Temperature Sensor * Example. * * \section temperature_usage Usage diff --git a/components/hdw-tft/hdw-tft.c b/components/hdw-tft/hdw-tft.c index 22663ec2d..8df456766 100644 --- a/components/hdw-tft/hdw-tft.c +++ b/components/hdw-tft/hdw-tft.c @@ -65,8 +65,8 @@ static inline uint32_t get_cCount() #define MIRROR_X false #define MIRROR_Y true #elif defined(CONFIG_ST7735_128x160) - // Mixture of docs + experimentation - // This is the RB027D25N05A / RB017D14N05A (Actually the ST7735S, so in between a ST7735 and ST7789) +// Mixture of docs + experimentation +// This is the RB027D25N05A / RB017D14N05A (Actually the ST7735S, so in between a ST7735 and ST7789) #define LCD_PIXEL_CLOCK_HZ (40 * 1000 * 1000) #define X_OFFSET 0 #define Y_OFFSET 0 @@ -88,7 +88,7 @@ static inline uint32_t get_cCount() #define MIRROR_X true #define MIRROR_Y true #elif defined(CONFIG_GC9307_240x280) - // A beautiful rounded edges LCD RB017A1505A +// A beautiful rounded edges LCD RB017A1505A #define LCD_PIXEL_CLOCK_HZ (80 * 1000 * 1000) #define X_OFFSET 20 #define Y_OFFSET 0 diff --git a/components/hdw-tft/include/hdw-tft.h b/components/hdw-tft/include/hdw-tft.h index 7a40f1dd8..9d09d7fff 100644 --- a/components/hdw-tft/include/hdw-tft.h +++ b/components/hdw-tft/include/hdw-tft.h @@ -3,7 +3,7 @@ * \section tft_design Design Philosophy * * TFT code is based on Espressif's LCD tjpgd + * href="https://github.com/espressif/esp-idf/tree/v5.1.1/examples/peripherals/lcd/tjpgd">Espressif's LCD tjpgd * example. * * Each pixel in the frame-buffer is of type ::paletteColor_t. diff --git a/components/hdw-usb/hdw-usb.c b/components/hdw-usb/hdw-usb.c index b5239a29b..89362ee1e 100644 --- a/components/hdw-usb/hdw-usb.c +++ b/components/hdw-usb/hdw-usb.c @@ -5,7 +5,6 @@ #include #include -#include "tinyusb.h" #include "hdw-usb.h" #include "advanced_usb_control.h" @@ -121,6 +120,7 @@ static const uint8_t hid_configuration_descriptor[] = { static fnAdvancedUsbHandler advancedUsbHandler; static fnSetSwadgeMode setSwadgeMode; +static const uint8_t* c_descriptor; //============================================================================== // Functions @@ -147,7 +147,8 @@ void initUsb(fnSetSwadgeMode _setSwadgeMode, fnAdvancedUsbHandler _advancedUsbHa .configuration_descriptor = hid_configuration_descriptor, }; - ESP_ERROR_CHECK(tinyusb_driver_install(&tusb_cfg)); + // Initialize TinyUSB with the default descriptor + initTusb(&tusb_cfg, hid_report_descriptor); // Set the log to print with advanced_usb_write_log_printf() esp_log_set_vprintf(advanced_usb_write_log_printf); @@ -155,6 +156,18 @@ void initUsb(fnSetSwadgeMode _setSwadgeMode, fnAdvancedUsbHandler _advancedUsbHa ESP_LOGI(TAG, "USB initialization DONE"); } +/** + * @brief Initialize TinyUSB + * + * @param tusb_cfg The TinyUSB configuration + * @param descriptor The descriptor to use for this configuration + */ +void initTusb(const tinyusb_config_t* tusb_cfg, const uint8_t* descriptor) +{ + c_descriptor = descriptor; + ESP_ERROR_CHECK(tinyusb_driver_install(tusb_cfg)); +} + /** * @brief Deinitialize USB HID device * Note, this does nothing as tinyusb_driver_uninstall() doesn't exist @@ -192,7 +205,7 @@ void sendUsbGamepadReport(hid_gamepad_report_t* report) uint8_t const* tud_hid_descriptor_report_cb(uint8_t instance __attribute__((unused))) { // We use only one interface and one HID report descriptor, so we can ignore parameter 'instance' - return hid_report_descriptor; + return c_descriptor; } /** diff --git a/components/hdw-usb/include/hdw-usb.h b/components/hdw-usb/include/hdw-usb.h index d07520faf..3cd96415a 100644 --- a/components/hdw-usb/include/hdw-usb.h +++ b/components/hdw-usb/include/hdw-usb.h @@ -3,9 +3,9 @@ * \section usb_design Design Philosophy * * The USB component uses Espressif's USB + * href="https://docs.espressif.com/projects/esp-idf/en/v5.1.1/esp32s2/api-reference/peripherals/usb_device.html">USB * Device Driver. It's based on the TinyUSB + * href="https://github.com/espressif/esp-idf/tree/v5.1.1/examples/peripherals/usb/device/tusb_hid">TinyUSB * Human Interface Device Example. * * The Swadge primarily functions as a USB gamepad. @@ -38,6 +38,7 @@ #define _HDW_USB_ #include +#include "tinyusb.h" /** * @brief Function typedef for a callback which will send USB SET_REPORT and GET_REPORT messages to a Swadge mode @@ -58,5 +59,6 @@ void initUsb(fnSetSwadgeMode setSwadgeMode, fnAdvancedUsbHandler advancedUsbHand void deinitUsb(void); void sendUsbGamepadReport(hid_gamepad_report_t* report); void usbSetSwadgeMode(void* newMode); +void initTusb(const tinyusb_config_t* tusb_cfg, const uint8_t* descriptor); #endif \ No newline at end of file diff --git a/components/wpa_supplicant/CMakeLists.txt b/components/wpa_supplicant/CMakeLists.txt deleted file mode 100644 index f57b4969c..000000000 --- a/components/wpa_supplicant/CMakeLists.txt +++ /dev/null @@ -1,3 +0,0 @@ -idf_component_register(SRCS "wpa_supplicant_minimal.c" - INCLUDE_DIRS "include" - REQUIRES esp_wifi) \ No newline at end of file diff --git a/components/wpa_supplicant/esp_wifi_driver.h b/components/wpa_supplicant/esp_wifi_driver.h deleted file mode 100644 index 538dfe7e1..000000000 --- a/components/wpa_supplicant/esp_wifi_driver.h +++ /dev/null @@ -1,303 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2019-2022 Espressif Systems (Shanghai) CO LTD - * - * SPDX-License-Identifier: Apache-2.0 - */ - -#ifndef _ESP_WIFI_DRIVER_H_ -#define _ESP_WIFI_DRIVER_H_ - -#include "esp_err.h" -#include "esp_wifi.h" - -#if CONFIG_NEWLIB_NANO_FORMAT - #define TASK_STACK_SIZE_ADD 0 -#else - #define TASK_STACK_SIZE_ADD 512 -#endif - -#define WPA2_TASK_STACK_SIZE (6144 + TASK_STACK_SIZE_ADD) -#define WPS_TASK_STACK_SIZE (12288 + TASK_STACK_SIZE_ADD) - -enum wpa_alg -{ - WIFI_WPA_ALG_NONE = 0, - WIFI_WPA_ALG_WEP40 = 1, - WIFI_WPA_ALG_TKIP = 2, - WIFI_WPA_ALG_CCMP = 3, - WIFI_WAPI_ALG_SMS4 = 4, - WIFI_WPA_ALG_WEP104 = 5, - WIFI_WPA_ALG_WEP = 6, - WIFI_WPA_ALG_IGTK = 7, - WIFI_WPA_ALG_PMK = 8, - WIFI_WPA_ALG_GCMP = 9, -}; - -typedef enum -{ - WIFI_APPIE_PROBEREQ = 0, - WIFI_APPIE_ASSOC_REQ, - WIFI_APPIE_ASSOC_RESP, - WIFI_APPIE_WPA, - WIFI_APPIE_RSN, - WIFI_APPIE_WPS_PR, - WIFI_APPIE_WPS_AR, - WIFI_APPIE_MESH_QUICK, - WIFI_APPIE_FREQ_ERROR, - WIFI_APPIE_ESP_MANUFACTOR, - WIFI_APPIE_COUNTRY, - WIFI_APPIE_MAX, -} wifi_appie_t; - -/* wifi_appie_t is in rom code and can't be changed anymore, use wifi_appie_ram_t for new app IEs */ -typedef enum -{ - WIFI_APPIE_RAM_BEACON = WIFI_APPIE_MAX, - WIFI_APPIE_RAM_PROBE_RSP, - WIFI_APPIE_RAM_STA_AUTH, - WIFI_APPIE_RAM_AP_AUTH, - WIFI_APPIE_RAM_MAX -} wifi_appie_ram_t; - -enum -{ - NONE_AUTH = 0x01, - WPA_AUTH_UNSPEC = 0x02, - WPA_AUTH_PSK = 0x03, - WPA2_AUTH_ENT = 0x04, - WPA2_AUTH_PSK = 0x05, - WPA_AUTH_CCKM = 0x06, - WPA2_AUTH_CCKM = 0x07, - WPA2_AUTH_PSK_SHA256 = 0x08, - WPA3_AUTH_PSK = 0x09, - WPA2_AUTH_ENT_SHA256 = 0x0a, - WAPI_AUTH_PSK = 0x0b, - WAPI_AUTH_CERT = 0x0c, - WPA2_AUTH_ENT_SHA384_SUITE_B = 0x0d, - WPA2_AUTH_FT_PSK = 0x0e, - WPA3_AUTH_OWE = 0x0f, - WPA2_AUTH_INVALID -}; - -typedef enum -{ - WPA2_ENT_EAP_STATE_NOT_START, - WPA2_ENT_EAP_STATE_IN_PROGRESS, - WPA2_ENT_EAP_STATE_SUCCESS, - WPA2_ENT_EAP_STATE_FAIL, -} wpa2_ent_eap_state_t; - -struct wifi_appie -{ - uint16_t ie_len; - uint8_t ie_data[]; -}; - -struct wifi_ssid -{ - int len; - uint8_t ssid[32]; -}; - -struct wps_scan_ie -{ - uint8_t* bssid; - uint8_t chan; - uint16_t capinfo; - uint8_t* ssid; - uint8_t* wpa; - uint8_t* rsn; - uint8_t* wps; -}; - -typedef struct -{ - int proto; - int pairwise_cipher; - int group_cipher; - int key_mgmt; - int capabilities; - size_t num_pmkid; - const u8* pmkid; - int mgmt_group_cipher; - uint8_t rsnxe_capa; -} wifi_wpa_ie_t; - -struct wpa_funcs -{ - bool (*wpa_sta_init)(void); - bool (*wpa_sta_deinit)(void); - int (*wpa_sta_connect)(uint8_t* bssid); - void (*wpa_sta_disconnected_cb)(uint8_t reason_code); - int (*wpa_sta_rx_eapol)(u8* src_addr, u8* buf, u32 len); - bool (*wpa_sta_in_4way_handshake)(void); - void* (*wpa_ap_init)(void); - bool (*wpa_ap_deinit)(void* data); - bool (*wpa_ap_join)(void** sm, u8* bssid, u8* wpa_ie, u8 wpa_ie_len, bool* pmf_enable); - bool (*wpa_ap_remove)(void* sm); - uint8_t* (*wpa_ap_get_wpa_ie)(uint8_t* len); - bool (*wpa_ap_rx_eapol)(void* hapd_data, void* sm, u8* data, size_t data_len); - void (*wpa_ap_get_peer_spp_msg)(void* sm, bool* spp_cap, bool* spp_req); - char* (*wpa_config_parse_string)(const char* value, size_t* len); - int (*wpa_parse_wpa_ie)(const u8* wpa_ie, size_t wpa_ie_len, wifi_wpa_ie_t* data); - int (*wpa_config_bss)(u8* bssid); - int (*wpa_michael_mic_failure)(u16 is_unicast); - uint8_t* (*wpa3_build_sae_msg)(uint8_t* bssid, uint32_t type, size_t* len); - int (*wpa3_parse_sae_msg)(uint8_t* buf, size_t len, uint32_t type, uint16_t status); - int (*wpa_sta_rx_mgmt)(u8 type, u8* frame, size_t len, u8* sender, u32 rssi, u8 channel, u64 current_tsf); - void (*wpa_config_done)(void); - uint8_t* (*owe_build_dhie)(uint16_t group); - int (*owe_process_assoc_resp)(const u8* rsn_ie, size_t rsn_len, const uint8_t* dh_ie, size_t dh_len); - int (*wpa_sta_set_ap_rsnxe)(const u8* rsnxe, size_t rsnxe_ie_len); -}; - -struct wpa2_funcs -{ - int (*wpa2_sm_rx_eapol)(u8* src_addr, u8* buf, u32 len, u8* bssid); - int (*wpa2_start)(void); - u8 (*wpa2_get_state)(void); - int (*wpa2_init)(void); - void (*wpa2_deinit)(void); -}; - -struct wps_funcs -{ - bool (*wps_parse_scan_result)(struct wps_scan_ie* scan); - int (*wifi_station_wps_start)(void); - int (*wps_sm_rx_eapol)(u8* src_addr, u8* buf, u32 len); - int (*wps_start_pending)(void); -}; - -typedef esp_err_t (*wifi_wpa2_fn_t)(void*); -typedef struct -{ - wifi_wpa2_fn_t fn; - void* param; -} wifi_wpa2_param_t; - -#define IS_WPS_REGISTRAR(type) (((type) > WPS_TYPE_MAX) ? (((type) < WPS_TYPE_MAX) ? true : false) : false) -#define IS_WPS_ENROLLEE(type) (((type) > WPS_TYPE_DISABLE) ? (((type) < WPS_TYPE_MAX) ? true : false) : false) - -typedef enum wps_status -{ - WPS_STATUS_DISABLE = 0, - WPS_STATUS_SCANNING, - WPS_STATUS_PENDING, - WPS_STATUS_SUCCESS, - WPS_STATUS_MAX, -} WPS_STATUS_t; - -#define WIFI_TXCB_EAPOL_ID 3 -typedef void (*wifi_tx_cb_t)(void*); -typedef int (*wifi_ipc_fn_t)(void*); -typedef struct -{ - wifi_ipc_fn_t fn; - void* arg; - uint32_t arg_size; -} wifi_ipc_config_t; - -#define WPA_IGTK_MAX_LEN 32 -typedef struct -{ - uint8_t keyid[2]; - uint8_t pn[6]; - uint8_t igtk[WPA_IGTK_MAX_LEN]; -} wifi_wpa_igtk_t; - -typedef struct -{ - wifi_interface_t ifx; - uint8_t subtype; - uint32_t data_len; - uint8_t data[0]; -} wifi_mgmt_frm_req_t; - -enum key_flag -{ - KEY_FLAG_MODIFY = BIT(0), - KEY_FLAG_DEFAULT = BIT(1), - KEY_FLAG_RX = BIT(2), - KEY_FLAG_TX = BIT(3), - KEY_FLAG_GROUP = BIT(4), - KEY_FLAG_PAIRWISE = BIT(5), - KEY_FLAG_PMK = BIT(6), -}; - -uint8_t* esp_wifi_ap_get_prof_pmk_internal(void); -struct wifi_ssid* esp_wifi_ap_get_prof_ap_ssid_internal(void); -uint8_t esp_wifi_ap_get_prof_authmode_internal(void); -uint8_t esp_wifi_sta_get_prof_authmode_internal(void); -uint8_t* esp_wifi_ap_get_prof_password_internal(void); -struct wifi_ssid* esp_wifi_sta_get_prof_ssid_internal(void); -uint8_t esp_wifi_sta_get_reset_param_internal(void); -uint8_t esp_wifi_sta_get_pairwise_cipher_internal(void); -uint8_t esp_wifi_sta_get_group_cipher_internal(void); -bool esp_wifi_sta_prof_is_wpa_internal(void); -int esp_wifi_get_macaddr_internal(uint8_t if_index, uint8_t* macaddr); -int esp_wifi_set_appie_internal(uint8_t type, uint8_t* ie, uint16_t len, uint8_t flag); -int esp_wifi_unset_appie_internal(uint8_t type); -struct wifi_appie* esp_wifi_get_appie_internal(uint8_t type); -void* esp_wifi_get_hostap_private_internal(void); // 1 -uint8_t* esp_wifi_sta_get_prof_password_internal(void); -void esp_wifi_deauthenticate_internal(u8 reason_code); -uint16_t esp_wifi_get_spp_attrubute_internal(uint8_t ifx); -bool esp_wifi_sta_is_running_internal(void); -bool esp_wifi_auth_done_internal(void); -int esp_wifi_set_ap_key_internal(int alg, const u8* addr, int idx, u8* key, size_t key_len); -int esp_wifi_set_sta_key_internal(int alg, u8* addr, int key_idx, int set_tx, u8* seq, size_t seq_len, u8* key, - size_t key_len, enum key_flag key_flag); -int esp_wifi_get_sta_key_internal(uint8_t* ifx, int* alg, u8* addr, int* key_idx, u8* key, size_t key_len, - enum key_flag key_flag); -bool esp_wifi_wpa_ptk_init_done_internal(uint8_t* mac); -uint8_t esp_wifi_sta_set_reset_param_internal(uint8_t reset_flag); -uint8_t esp_wifi_get_sta_gtk_index_internal(void); -int esp_wifi_register_tx_cb_internal(wifi_tx_cb_t fn, u8 id); -int esp_wifi_register_wpa_cb_internal(struct wpa_funcs* cb); -int esp_wifi_unregister_wpa_cb_internal(void); -int esp_wifi_get_assoc_bssid_internal(uint8_t* bssid); -bool esp_wifi_sta_is_ap_notify_completed_rsne_internal(void); -int esp_wifi_ap_deauth_internal(uint8_t* mac, uint32_t reason); -int esp_wifi_ipc_internal(wifi_ipc_config_t* cfg, bool sync); -int esp_wifi_register_wpa2_cb_internal(struct wpa2_funcs* cb); -int esp_wifi_unregister_wpa2_cb_internal(void); -bool esp_wifi_sta_prof_is_wpa2_internal(void); -bool esp_wifi_sta_prof_is_rsn_internal(void); -bool esp_wifi_sta_prof_is_wapi_internal(void); -esp_err_t esp_wifi_sta_wpa2_ent_disable_internal(wifi_wpa2_param_t* param); -esp_err_t esp_wifi_sta_wpa2_ent_enable_internal(wifi_wpa2_param_t* param); -esp_err_t esp_wifi_set_wpa2_ent_state_internal(wpa2_ent_eap_state_t state); -int esp_wifi_get_wps_type_internal(void); -int esp_wifi_set_wps_type_internal(uint32_t type); -int esp_wifi_get_wps_status_internal(void); -int esp_wifi_set_wps_status_internal(uint32_t status); -int esp_wifi_disarm_sta_connection_timer_internal(void); -bool esp_wifi_get_sniffer_internal(void); -int esp_wifi_set_wps_cb_internal(struct wps_funcs* wps_cb); -bool esp_wifi_enable_sta_privacy_internal(void); -uint8_t esp_wifi_get_user_init_flag_internal(void); -esp_err_t esp_wifi_internal_supplicant_header_md5_check(const char* md5); -int esp_wifi_sta_update_ap_info_internal(void); -uint8_t* esp_wifi_sta_get_ap_info_prof_pmk_internal(void); -esp_err_t esp_wifi_set_wps_start_flag_internal(bool start); -uint16_t esp_wifi_sta_pmf_enabled(void); -wifi_cipher_type_t esp_wifi_sta_get_mgmt_group_cipher(void); -int esp_wifi_set_igtk_internal(uint8_t if_index, const wifi_wpa_igtk_t* igtk); -esp_err_t esp_wifi_internal_issue_disconnect(uint8_t reason_code); -bool esp_wifi_skip_supp_pmkcaching(void); -bool esp_wifi_is_rm_enabled_internal(uint8_t if_index); -bool esp_wifi_is_btm_enabled_internal(uint8_t if_index); -esp_err_t esp_wifi_register_mgmt_frame_internal(uint32_t type, uint32_t subtype); -esp_err_t esp_wifi_send_mgmt_frm_internal(const wifi_mgmt_frm_req_t* req); -uint8_t esp_wifi_ap_get_prof_pairwise_cipher_internal(void); -esp_err_t esp_wifi_action_tx_req(uint8_t type, uint8_t channel, uint32_t wait_time_ms, const wifi_action_tx_req_t* req); -esp_err_t esp_wifi_remain_on_channel(uint8_t ifx, uint8_t type, uint8_t channel, uint32_t wait_time_ms, - wifi_action_rx_cb_t rx_cb); -bool esp_wifi_is_mbo_enabled_internal(uint8_t if_index); -void esp_wifi_get_pmf_config_internal(wifi_pmf_config_t* pmf_cfg, uint8_t ifx); -bool esp_wifi_is_ft_enabled_internal(uint8_t if_index); -uint8_t esp_wifi_sta_get_config_sae_pwe_h2e_internal(void); -uint8_t esp_wifi_sta_get_use_h2e_internal(void); -void esp_wifi_sta_disable_wpa2_authmode_internal(void); - -#endif /* _ESP_WIFI_DRIVER_H_ */ diff --git a/components/wpa_supplicant/include/esp_wpa.h b/components/wpa_supplicant/include/esp_wpa.h deleted file mode 100644 index 568accfdb..000000000 --- a/components/wpa_supplicant/include/esp_wpa.h +++ /dev/null @@ -1,73 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2019-2022 Espressif Systems (Shanghai) CO LTD - * - * SPDX-License-Identifier: Apache-2.0 - */ - -#ifndef __ESP_WPA_H__ -#define __ESP_WPA_H__ - -#include -#include -#include "esp_err.h" -#include "esp_wifi_crypto_types.h" -#include "esp_wifi_types.h" - -#ifdef __cplusplus -extern "C" -{ -#endif - - /** \defgroup WiFi_APIs WiFi Related APIs - * @brief WiFi APIs - */ - - /** @addtogroup WiFi_APIs - * @{ - */ - - /** \defgroup WPA_APIs WPS APIs - * @brief Supplicant APIs - * - */ - - /** @addtogroup WPA_APIs - * @{ - */ - /* Crypto callback functions */ - extern const wpa_crypto_funcs_t g_wifi_default_wpa_crypto_funcs; // NOLINT(readability-redundant-declaration) - - /* Mesh crypto callback functions */ - extern const mesh_crypto_funcs_t g_wifi_default_mesh_crypto_funcs; - - /** - * @brief Supplicant initialization - * - * @return - * - ESP_OK : succeed - * - ESP_ERR_NO_MEM : out of memory - */ - esp_err_t esp_supplicant_init(void); - - /** - * @brief Supplicant deinitialization - * - * @return - * - ESP_OK : succeed - * - others: failed - */ - esp_err_t esp_supplicant_deinit(void); - - /** - * @} - */ - - /** - * @} - */ - -#ifdef __cplusplus -} -#endif - -#endif /* __ESP_WPA_H__ */ diff --git a/components/wpa_supplicant/include/os.h b/components/wpa_supplicant/include/os.h deleted file mode 100644 index 49d25aa11..000000000 --- a/components/wpa_supplicant/include/os.h +++ /dev/null @@ -1,390 +0,0 @@ -/* - * OS specific functions - * Copyright (c) 2005-2009, Jouni Malinen - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 2 as - * published by the Free Software Foundation. - * - * Alternatively, this software may be distributed under the terms of BSD - * license. - * - * See README and COPYING for more details. - */ - -#ifndef OS_H -#define OS_H -#include "esp_types.h" -#include -#include -#include -#include "esp_err.h" -#include "supplicant_opt.h" -#include "esp_wifi.h" - -typedef time_t os_time_t; - -/** - * os_sleep - Sleep (sec, usec) - * @sec: Number of seconds to sleep - * @usec: Number of microseconds to sleep - */ -void os_sleep(os_time_t sec, os_time_t usec); - -struct os_time -{ - os_time_t sec; - suseconds_t usec; -}; - -#define os_reltime os_time - -struct os_tm -{ - int sec; /* 0..59 or 60 for leap seconds */ - int min; /* 0..59 */ - int hour; /* 0..23 */ - int day; /* 1..31 */ - int month; /* 1..12 */ - int year; /* Four digit year */ -}; - -/** - * os_get_time - Get current time (sec, usec) - * @t: Pointer to buffer for the time - * Returns: 0 on success, -1 on failure - */ -int os_get_time(struct os_time* t); -#define os_get_reltime os_get_time - -/* Helper macros for handling struct os_time */ - -#define os_time_before(a, b) ((a)->sec < (b)->sec || ((a)->sec == (b)->sec && (a)->usec < (b)->usec)) - -#define os_reltime_before os_time_before -#define os_time_sub(a, b, res) \ - do \ - { \ - (res)->sec = (a)->sec - (b)->sec; \ - (res)->usec = (a)->usec - (b)->usec; \ - if ((res)->usec < 0) \ - { \ - (res)->sec--; \ - (res)->usec += 1000000; \ - } \ - } while (0) -#define os_reltime_sub os_time_sub - -/** - * os_mktime - Convert broken-down time into seconds since 1970-01-01 - * @year: Four digit year - * @month: Month (1 .. 12) - * @day: Day of month (1 .. 31) - * @hour: Hour (0 .. 23) - * @min: Minute (0 .. 59) - * @sec: Second (0 .. 60) - * @t: Buffer for returning calendar time representation (seconds since - * 1970-01-01 00:00:00) - * Returns: 0 on success, -1 on failure - * - * Note: The result is in seconds from Epoch, i.e., in UTC, not in local time - * which is used by POSIX mktime(). - */ -int os_mktime(int year, int month, int day, int hour, int min, int sec, os_time_t* t); - -int os_gmtime(os_time_t t, struct os_tm* tm); - -/** - * os_daemonize - Run in the background (detach from the controlling terminal) - * @pid_file: File name to write the process ID to or %NULL to skip this - * Returns: 0 on success, -1 on failure - */ -int os_daemonize(const char* pid_file); - -/** - * os_daemonize_terminate - Stop running in the background (remove pid file) - * @pid_file: File name to write the process ID to or %NULL to skip this - */ -void os_daemonize_terminate(const char* pid_file); - -/** - * os_get_random - Get cryptographically strong pseudo random data - * @buf: Buffer for pseudo random data - * @len: Length of the buffer - * Returns: 0 on success, -1 on failure - */ -int os_get_random(unsigned char* buf, size_t len); - -/** - * os_random - Get pseudo random value (not necessarily very strong) - * Returns: Pseudo random value - */ -unsigned long os_random(void); - -/** - * os_rel2abs_path - Get an absolute path for a file - * @rel_path: Relative path to a file - * Returns: Absolute path for the file or %NULL on failure - * - * This function tries to convert a relative path of a file to an absolute path - * in order for the file to be found even if current working directory has - * changed. The returned value is allocated and caller is responsible for - * freeing it. It is acceptable to just return the same path in an allocated - * buffer, e.g., return strdup(rel_path). This function is only used to find - * configuration files when os_daemonize() may have changed the current working - * directory and relative path would be pointing to a different location. - */ -char* os_rel2abs_path(const char* rel_path); - -/** - * os_program_init - Program initialization (called at start) - * Returns: 0 on success, -1 on failure - * - * This function is called when a programs starts. If there are any OS specific - * processing that is needed, it can be placed here. It is also acceptable to - * just return 0 if not special processing is needed. - */ -int os_program_init(void); - -/** - * os_program_deinit - Program deinitialization (called just before exit) - * - * This function is called just before a program exists. If there are any OS - * specific processing, e.g., freeing resourced allocated in os_program_init(), - * it should be done here. It is also acceptable for this function to do - * nothing. - */ -void os_program_deinit(void); - -/** - * os_setenv - Set environment variable - * @name: Name of the variable - * @value: Value to set to the variable - * @overwrite: Whether existing variable should be overwritten - * Returns: 0 on success, -1 on error - * - * This function is only used for wpa_cli action scripts. OS wrapper does not - * need to implement this if such functionality is not needed. - */ -int os_setenv(const char* name, const char* value, int overwrite); - -/** - * os_unsetenv - Delete environent variable - * @name: Name of the variable - * Returns: 0 on success, -1 on error - * - * This function is only used for wpa_cli action scripts. OS wrapper does not - * need to implement this if such functionality is not needed. - */ -int os_unsetenv(const char* name); - -/** - * os_readfile - Read a file to an allocated memory buffer - * @name: Name of the file to read - * @len: For returning the length of the allocated buffer - * Returns: Pointer to the allocated buffer or %NULL on failure - * - * This function allocates memory and reads the given file to this buffer. Both - * binary and text files can be read with this function. The caller is - * responsible for freeing the returned buffer with os_free(). - */ -/* We don't support file reading support */ -static inline char* os_readfile(const char* name, size_t* len) -{ - return NULL; -} - -/* - * The following functions are wrapper for standard ANSI C or POSIX functions. - * By default, they are just defined to use the standard function name and no - * os_*.c implementation is needed for them. This avoids extra function calls - * by allowing the C pre-processor take care of the function name mapping. - * - * If the target system uses a C library that does not provide these functions, - * build_config.h can be used to define the wrappers to use a different - * function name. This can be done on function-by-function basis since the - * defines here are only used if build_config.h does not define the os_* name. - * If needed, os_*.c file can be used to implement the functions that are not - * included in the C library on the target system. Alternatively, - * OS_NO_C_LIB_DEFINES can be defined to skip all defines here in which case - * these functions need to be implemented in os_*.c file for the target system. - */ - -#ifndef os_malloc - #define os_malloc(s) malloc((s)) -#endif -#ifndef os_realloc - #define os_realloc(p, s) realloc((p), (s)) -#endif -#ifndef os_zalloc - #define os_zalloc(s) calloc(1, (s)) -#endif -#ifndef os_calloc - #define os_calloc(p, s) calloc((p), (s)) -#endif - -#ifndef os_free - #define os_free(p) free((p)) -#endif - -#ifndef os_bzero - #define os_bzero(s, n) bzero(s, n) -#endif - -#ifndef os_strdup - #ifdef _MSC_VER - #define os_strdup(s) _strdup(s) - #else - #define os_strdup(s) strdup(s) - #endif -#endif -char* ets_strdup(const char* s); - -#ifndef os_memcpy - #define os_memcpy(d, s, n) memcpy((d), (s), (n)) -#endif -#ifndef os_memmove - #define os_memmove(d, s, n) memmove((d), (s), (n)) -#endif -#ifndef os_memset - #define os_memset(s, c, n) memset(s, c, n) -#endif -#ifndef os_memcmp - #define os_memcmp(s1, s2, n) memcmp((s1), (s2), (n)) -#endif -#ifndef os_memcmp_const - #define os_memcmp_const(s1, s2, n) memcmp((s1), (s2), (n)) -#endif - -#ifndef os_strlen - #define os_strlen(s) strlen(s) -#endif -#ifndef os_strcasecmp - #ifdef _MSC_VER - #define os_strcasecmp(s1, s2) _stricmp((s1), (s2)) - #else - #define os_strcasecmp(s1, s2) strcasecmp((s1), (s2)) - #endif -#endif -#ifndef os_strncasecmp - #ifdef _MSC_VER - #define os_strncasecmp(s1, s2, n) _strnicmp((s1), (s2), (n)) - #else - #define os_strncasecmp(s1, s2, n) strncasecmp((s1), (s2), (n)) - #endif -#endif -#ifndef os_strchr - #define os_strchr(s, c) strchr((s), (c)) -#endif -#ifndef os_strcmp - #define os_strcmp(s1, s2) strcmp((s1), (s2)) -#endif -#ifndef os_strncmp - #define os_strncmp(s1, s2, n) strncmp((s1), (s2), (n)) -#endif -#ifndef os_strrchr - #define os_strrchr(s, c) strrchr((s), (c)) -#endif -#ifndef os_strstr - #define os_strstr(h, n) strstr((h), (n)) -#endif -#ifndef os_strlcpy - #define os_strlcpy(d, s, n) strlcpy((d), (s), (n)) -#endif -#ifndef os_strcat - #define os_strcat(d, s) strcat((d), (s)) -#endif - -#ifndef os_snprintf - #ifdef _MSC_VER - #define os_snprintf _snprintf - #else - #define os_snprintf snprintf - #endif -#endif -#ifndef os_sprintf - #define os_sprintf sprintf -#endif - -static inline int os_snprintf_error(size_t size, int res) -{ - return res < 0 || (unsigned int)res >= size; -} - -static inline void* os_realloc_array(void* ptr, size_t nmemb, size_t size) -{ - if (size && nmemb > (~(size_t)0) / size) - return NULL; - return os_realloc(ptr, nmemb * size); -} - -#ifdef CONFIG_CRYPTO_MBEDTLS -void forced_memzero(void* ptr, size_t len); -#else -/* Try to prevent most compilers from optimizing out clearing of memory that - * becomes unaccessible after this function is called. This is mostly the case - * for clearing local stack variables at the end of a function. This is not - * exactly perfect, i.e., someone could come up with a compiler that figures out - * the pointer is pointing to memset and then end up optimizing the call out, so - * try go a bit further by storing the first octet (now zero) to make this even - * a bit more difficult to optimize out. Once memset_s() is available, that - * could be used here instead. */ -static void* (*const volatile memset_func)(void*, int, size_t) = memset; -static uint8_t forced_memzero_val; - -static inline void forced_memzero(void* ptr, size_t len) -{ - memset_func(ptr, 0, len); - if (len) - { - forced_memzero_val = ((uint8_t*)ptr)[0]; - } -} -#endif - -extern const wifi_osi_funcs_t* wifi_funcs; -#define OS_BLOCK OSI_FUNCS_TIME_BLOCKING - -#define os_mutex_lock(a) wifi_funcs->_mutex_lock((a)) -#define os_mutex_unlock(a) wifi_funcs->_mutex_unlock((a)) -#define os_recursive_mutex_create() wifi_funcs->_recursive_mutex_create() - -#define os_queue_create(a, b) wifi_funcs->_queue_create((a), (b)) -#define os_queue_delete(a) wifi_funcs->_queue_delete(a) -#define os_queue_send(a, b, c) wifi_funcs->_queue_send((a), (b), (c)) -#define os_queue_recv(a, b, c) wifi_funcs->_queue_recv((a), (b), (c)) - -#define os_task_create(a, b, c, d, e, f) wifi_funcs->_task_create((a), (b), (c), (d), (e), (f)) -#define os_task_delete(a) wifi_funcs->_task_delete((a)) -#define os_task_get_current_task() wifi_funcs->_task_get_current_task() - -#define os_semphr_create(a, b) wifi_funcs->_semphr_create((a), (b)) -#define os_semphr_delete(a) wifi_funcs->_semphr_delete((a)) -#define os_semphr_give(a) wifi_funcs->_semphr_give((a)) -#define os_semphr_take(a, b) wifi_funcs->_semphr_take((a), (b)) - -#define os_task_ms_to_tick(a) wifi_funcs->_task_ms_to_tick((a)) -#define os_timer_get_time(void) wifi_funcs->_esp_timer_get_time(void) - -static inline void os_timer_setfn(void* ptimer, void* pfunction, void* parg) -{ - return wifi_funcs->_timer_setfn(ptimer, pfunction, parg); -} -static inline void os_timer_disarm(void* ptimer) -{ - return wifi_funcs->_timer_disarm(ptimer); -} -static inline void os_timer_arm_us(void* ptimer, uint32_t u_seconds, bool repeat_flag) -{ - return wifi_funcs->_timer_arm_us(ptimer, u_seconds, repeat_flag); -} -static inline void os_timer_arm(void* ptimer, uint32_t milliseconds, bool repeat_flag) -{ - return wifi_funcs->_timer_arm(ptimer, milliseconds, repeat_flag); -} -static inline void os_timer_done(void* ptimer) -{ - return wifi_funcs->_timer_done(ptimer); -} - -#endif /* OS_H */ diff --git a/components/wpa_supplicant/include/supplicant_opt.h b/components/wpa_supplicant/include/supplicant_opt.h deleted file mode 100644 index 1ec4ec5b4..000000000 --- a/components/wpa_supplicant/include/supplicant_opt.h +++ /dev/null @@ -1,23 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2019-2022 Espressif Systems (Shanghai) CO LTD - * - * SPDX-License-Identifier: Apache-2.0 - */ - -#ifndef _SUPPLICANT_OPT_H -#define _SUPPLICANT_OPT_H - -#include "sdkconfig.h" - -#if CONFIG_WPA_DEBUG_PRINT - #define DEBUG_PRINT - #if defined(CONFIG_LOG_DEFAULT_LEVEL_DEBUG) || defined(CONFIG_LOG_DEFAULT_LEVEL_VERBOSE) - #define ELOOP_DEBUG - #endif -#endif - -#if CONFIG_WPA_SCAN_CACHE - #define SCAN_CACHE_SUPPORTED -#endif - -#endif /* _SUPPLICANT_OPT_H */ diff --git a/components/wpa_supplicant/wpa_supplicant_minimal.c b/components/wpa_supplicant/wpa_supplicant_minimal.c deleted file mode 100644 index 1c35476ba..000000000 --- a/components/wpa_supplicant/wpa_supplicant_minimal.c +++ /dev/null @@ -1,464 +0,0 @@ -#include -#include -#include -#include -#include - -#include "esp_random.h" -#include "esp_log.h" - -#include "esp_wpa.h" -#include "os.h" - -typedef uint64_t u64; -typedef uint32_t u32; -typedef uint16_t u16; -typedef uint8_t u8; -typedef int64_t s64; -typedef int32_t s32; -typedef int16_t s16; -typedef int8_t s8; - -#include "esp_wifi_driver.h" - -const char WPA_TAG[] = "WPA"; - -static struct wpa_funcs* wpa_cb; - -//////////////////////////////////////////////////////////////////////////////// - -bool wpa_attach(void) -{ - ESP_LOGI(WPA_TAG, "%s", __func__); - return true; -} - -bool wpa_deattach(void) -{ - ESP_LOGI(WPA_TAG, "%s", __func__); - return true; -} - -// int wpa_sta_connect(uint8_t *bssid) -// { -// ESP_LOGI(WPA_TAG,"%s",__func__); -// return 0; -// } - -// void wpa_sta_disconnected_cb(uint8_t reason_code) -// { -// ESP_LOGI(WPA_TAG,"%s",__func__); -// } - -// int wpa_sm_rx_eapol(u8 *src_addr, u8 *buf, u32 len) -// { -// ESP_LOGI(WPA_TAG,"%s",__func__); -// return 0; -// } - -// bool wpa_sta_in_4way_handshake(void) -// { -// ESP_LOGI(WPA_TAG,"%s",__func__); -// return true; -// } - -// #ifdef CONFIG_ESP_WIFI_SOFTAP_SUPPORT - -// void *hostap_init(void) -// { -// ESP_LOGI(WPA_TAG,"%s",__func__); -// return NULL; -// } - -// bool hostap_deinit(void *data) -// { -// ESP_LOGI(WPA_TAG,"%s",__func__); -// return true; -// } - -// bool wpa_ap_join(void **sm, u8 *bssid, u8 *wpa_ie, u8 wpa_ie_len) -// { -// ESP_LOGI(WPA_TAG,"%s",__func__); -// return true; -// } - -// bool wpa_ap_remove(void *sm) -// { -// ESP_LOGI(WPA_TAG,"%s",__func__); -// return true; -// } - -// uint8_t *wpa_ap_get_wpa_ie(uint8_t *len) -// { -// ESP_LOGI(WPA_TAG,"%s",__func__); -// return NULL; -// } - -// bool wpa_ap_rx_eapol(void *hapd_data, void *sm, u8 *data, size_t data_len) -// { -// ESP_LOGI(WPA_TAG,"%s",__func__); -// return true; -// } - -// void wpa_ap_get_peer_spp_msg(void *sm, bool *spp_cap, bool *spp_req) -// { -// ESP_LOGI(WPA_TAG,"%s",__func__); -// } - -// #endif - -// char *wpa_config_parse_string(const char *value, size_t *len) -// { -// ESP_LOGI(WPA_TAG,"%s",__func__); -// return NULL; -// } - -// int wpa_parse_wpa_ie_wrapper(const u8 *wpa_ie, size_t wpa_ie_len, wifi_wpa_ie_t *data) -// { -// ESP_LOGI(WPA_TAG,"%s",__func__); -// return 0; -// } - -// #if 0 - -// int wpa_config_bss(u8 *bssid) -// { -// ESP_LOGI(WPA_TAG,"%s",__func__); -// return 0; -// } - -// #endif - -// int wpa_michael_mic_failure(u16 is_unicast) -// { -// ESP_LOGI(WPA_TAG,"%s",__func__); -// return 0; -// } - -// #if 0 - -// uint8_t *wpa3_build_sae_msg(uint8_t *bssid, uint32_t type, size_t *len) -// { -// ESP_LOGI(WPA_TAG,"%s",__func__); -// return NULL; -// } - -// int wpa3_parse_sae_msg(uint8_t *buf, size_t len, uint32_t type, uint16_t status) -// { -// ESP_LOGI(WPA_TAG,"%s",__func__); -// return 0; -// } - -// int wpa_sta_rx_mgmt(u8 type, u8 *frame, size_t len, u8 *sender, u32 rssi, u8 channel, u64 current_tsf) -// { -// ESP_LOGI(WPA_TAG,"%s",__func__); -// return 0; -// } - -// #endif - -void wpa_config_done(void) -{ - ESP_LOGI(WPA_TAG, "%s", __func__); -} - -// #if 0 - -// bool wpa_sta_profile_match(u8 *bssid) -// { -// ESP_LOGI(WPA_TAG,"%s",__func__); -// return true; -// } - -// #endif - -int esp_supplicant_init(void) -{ - int ret = ESP_OK; - - wpa_cb = (struct wpa_funcs*)os_zalloc(sizeof(struct wpa_funcs)); - if (!wpa_cb) - { - return ESP_ERR_NO_MEM; - } - - wpa_cb->wpa_sta_init = wpa_attach; - wpa_cb->wpa_sta_deinit = NULL; // wpa_deattach; - wpa_cb->wpa_sta_rx_eapol = NULL; // ; - wpa_cb->wpa_sta_connect = NULL; // wpa_sta_connect; - wpa_cb->wpa_sta_disconnected_cb = NULL; // wpa_sta_disconnected_cb; - wpa_cb->wpa_sta_in_4way_handshake = NULL; // wpa_sta_in_4way_handshake; - -#ifdef CONFIG_ESP_WIFI_SOFTAP_SUPPORT - wpa_cb->wpa_ap_join = NULL; // wpa_ap_join; - wpa_cb->wpa_ap_remove = NULL; // wpa_ap_remove; - wpa_cb->wpa_ap_get_wpa_ie = NULL; // wpa_ap_get_wpa_ie; - wpa_cb->wpa_ap_rx_eapol = NULL; // wpa_ap_rx_eapol; - wpa_cb->wpa_ap_get_peer_spp_msg = NULL; // wpa_ap_get_peer_spp_msg; - wpa_cb->wpa_ap_init = NULL; // hostap_init; - wpa_cb->wpa_ap_deinit = NULL; // hostap_deinit; -#endif - - wpa_cb->wpa_config_parse_string = NULL; // wpa_config_parse_string; - wpa_cb->wpa_parse_wpa_ie = NULL; // wpa_parse_wpa_ie_wrapper; - wpa_cb->wpa_config_bss = NULL; // wpa_config_bss; - wpa_cb->wpa_michael_mic_failure = NULL; // wpa_michael_mic_failure; - wpa_cb->wpa_config_done = wpa_config_done; - -#ifdef CONFIG_WPA3_SAE - esp_wifi_register_wpa3_cb(wpa_cb); -#endif - // ret = esp_supplicant_common_init(wpa_cb); - - if (ret != 0) - { - return ret; - } - - esp_wifi_register_wpa_cb_internal(wpa_cb); - -#if CONFIG_WPA_WAPI_PSK - ret = esp_wifi_internal_wapi_init(); -#endif - - return ret; -} - -int esp_supplicant_deinit(void) -{ - // esp_supplicant_common_deinit(); - esp_err_t ret = esp_wifi_unregister_wpa_cb_internal(); - if (wpa_cb) - { - free(wpa_cb); - } - return ret; -} - -//////////////////////////////////////////////////////////////////////////////// - -int os_get_time(struct os_time* t) -{ - struct timeval tv; - int ret = gettimeofday(&tv, NULL); - t->sec = (os_time_t)tv.tv_sec; - t->usec = tv.tv_usec; - return ret; -} - -unsigned long os_random(void) -{ - return esp_random(); -} - -int os_get_random(unsigned char* buf, size_t len) -{ - esp_fill_random(buf, len); - return 0; -} - -//////////////////////////////////////////////////////////////////////////////// - -// int aes_128_cbc_encrypt(const unsigned char *key, const unsigned char *iv, unsigned char *data, int data_len) -// { -// ESP_LOGI(WPA_TAG, "%s", __func__); -// return 0; -// } - -// int aes_128_cbc_decrypt(const unsigned char *key, const unsigned char *iv, unsigned char *data, int data_len) -// { -// ESP_LOGI(WPA_TAG, "%s", __func__); -// return 0; -// } - -// int esp_aes_wrap(const unsigned char *kek, int n, const unsigned char *plain, unsigned char *cipher) -// { -// ESP_LOGI(WPA_TAG, "%s", __func__); -// return 0; -// } - -// int esp_aes_unwrap(const unsigned char *kek, int n, const unsigned char *cipher, unsigned char *plain) -// { -// ESP_LOGI(WPA_TAG, "%s", __func__); -// return 0; -// } - -// int hmac_sha256_vector(const unsigned char *key, int key_len, int num_elem, -// const unsigned char *addr[], const int *len, unsigned char *mac) -// { -// ESP_LOGI(WPA_TAG, "%s", __func__); -// return 0; -// } - -// int sha256_prf(const unsigned char *key, int key_len, const char *label, -// const unsigned char *data, int data_len, unsigned char *buf, int buf_len) -// { -// ESP_LOGI(WPA_TAG, "%s", __func__); -// return 0; -// } - -// int hmac_md5(const unsigned char *key, unsigned int key_len, const unsigned char *data, -// unsigned int data_len, unsigned char *mac) -// { -// ESP_LOGI(WPA_TAG, "%s", __func__); -// return 0; -// } - -// int hmac_md5_vector(const unsigned char *key, unsigned int key_len, unsigned int num_elem, -// const unsigned char *addr[], const unsigned int *len, unsigned char *mac) -// { -// ESP_LOGI(WPA_TAG, "%s", __func__); -// return 0; -// } - -// int hmac_sha1(const unsigned char *key, unsigned int key_len, const unsigned char *data, -// unsigned int data_len, unsigned char *mac) -// { -// ESP_LOGI(WPA_TAG, "%s", __func__); -// return 0; -// } - -// int hmac_sha1_vector(const unsigned char *key, unsigned int key_len, unsigned int num_elem, -// const unsigned char *addr[], const unsigned int *len, unsigned char *mac) -// { -// ESP_LOGI(WPA_TAG, "%s", __func__); -// return 0; -// } - -// int sha1_prf(const unsigned char *key, unsigned int key_len, const char *label, -// const unsigned char *data, unsigned int data_len, unsigned char *buf, unsigned int -// buf_len) -// { -// ESP_LOGI(WPA_TAG, "%s", __func__); -// return 0; -// } - -// int sha1_vector(unsigned int num_elem, const unsigned char *addr[], const unsigned int *len, -// unsigned char *mac) -// { -// ESP_LOGI(WPA_TAG, "%s", __func__); -// return 0; -// } - -// int pbkdf2_sha1(const char *passphrase, const char *ssid, unsigned int ssid_len, -// int iterations, unsigned char *buf, unsigned int buflen) -// { -// ESP_LOGI(WPA_TAG, "%s", __func__); -// return 0; -// } - -// int rc4_skip(const unsigned char *key, unsigned int keylen, unsigned int skip, -// unsigned char *data, unsigned int data_len) -// { -// ESP_LOGI(WPA_TAG, "%s", __func__); -// return 0; -// } - -// int md5_vector(unsigned int num_elem, const unsigned char *addr[], const unsigned int *len, -// unsigned char *mac) -// { -// ESP_LOGI(WPA_TAG, "%s", __func__); -// return 0; -// } - -// void esp_aes_encrypt(void *ctx, const unsigned char *plain, unsigned char *crypt) -// { -// ESP_LOGI(WPA_TAG, "%s", __func__); -// } - -// void * aes_encrypt_init(const unsigned char *key, unsigned int len) -// { -// ESP_LOGI(WPA_TAG, "%s", __func__); -// return NULL; -// } - -// void aes_encrypt_deinit(void *ctx) -// { -// ESP_LOGI(WPA_TAG, "%s", __func__); -// } - -// void esp_aes_decrypt(void *ctx, const unsigned char *crypt, unsigned char *plain) -// { -// ESP_LOGI(WPA_TAG, "%s", __func__); -// } - -// void * aes_decrypt_init(const unsigned char *key, unsigned int len) -// { -// ESP_LOGI(WPA_TAG, "%s", __func__); -// return NULL; -// } - -// void aes_decrypt_deinit(void *ctx) -// { -// ESP_LOGI(WPA_TAG, "%s", __func__); -// } - -// int omac1_aes_128(const uint8_t *key, const uint8_t *data, size_t data_len, -// uint8_t *mic) -// { -// ESP_LOGI(WPA_TAG, "%s", __func__); -// return 0; -// } - -// uint8_t * ccmp_decrypt(const uint8_t *tk, const uint8_t *ieee80211_hdr, -// const uint8_t *data, size_t data_len, -// size_t *decrypted_len, bool espnow_pkt) -// { -// ESP_LOGI(WPA_TAG, "%s", __func__); -// return NULL; -// } - -// uint8_t * ccmp_encrypt(const uint8_t *tk, uint8_t *frame, size_t len, size_t hdrlen, -// uint8_t *pn, int keyid, size_t *encrypted_len) -// { -// ESP_LOGI(WPA_TAG, "%s", __func__); -// return NULL; -// } - -// int esp_aes_gmac(const uint8_t *key, size_t keylen, const uint8_t *iv, size_t iv_len, -// const uint8_t *aad, size_t aad_len, uint8_t *mic) -// { -// ESP_LOGI(WPA_TAG, "%s", __func__); -// return 0; -// } - -/* - * This structure is used to set the cyrpto callback function for station to connect when in security mode. - * These functions either call MbedTLS API's if USE_MBEDTLS_CRYPTO flag is set through Kconfig, or native - * API's otherwise. We recommend setting the flag since MbedTLS API's utilize hardware acceleration while - * native API's are use software implementations. - */ -const wpa_crypto_funcs_t g_wifi_default_wpa_crypto_funcs = { - .size = sizeof(wpa_crypto_funcs_t), - .version = ESP_WIFI_CRYPTO_VERSION, - .aes_wrap = NULL, // (esp_aes_wrap_t)esp_aes_wrap, - .aes_unwrap = NULL, // (esp_aes_unwrap_t)esp_aes_unwrap, - .hmac_sha256_vector = NULL, // (esp_hmac_sha256_vector_t)hmac_sha256_vector, - .sha256_prf = NULL, // (esp_sha256_prf_t)sha256_prf, - .hmac_md5 = NULL, // (esp_hmac_md5_t)hmac_md5, - .hamc_md5_vector = NULL, // (esp_hmac_md5_vector_t)hmac_md5_vector, - .hmac_sha1 = NULL, // (esp_hmac_sha1_t)hmac_sha1, - .hmac_sha1_vector = NULL, // (esp_hmac_sha1_vector_t)hmac_sha1_vector, - .sha1_prf = NULL, // (esp_sha1_prf_t)sha1_prf, - .sha1_vector = NULL, // (esp_sha1_vector_t)sha1_vector, - .pbkdf2_sha1 = NULL, // (esp_pbkdf2_sha1_t)pbkdf2_sha1, - .rc4_skip = NULL, // (esp_rc4_skip_t)rc4_skip, - .md5_vector = NULL, // (esp_md5_vector_t)md5_vector, - .aes_encrypt = NULL, // (esp_aes_encrypt_t)esp_aes_encrypt, - .aes_encrypt_init = NULL, // (esp_aes_encrypt_init_t)aes_encrypt_init, - .aes_encrypt_deinit = NULL, // (esp_aes_encrypt_deinit_t)aes_encrypt_deinit, - .aes_decrypt = NULL, // (esp_aes_decrypt_t)esp_aes_decrypt, - .aes_decrypt_init = NULL, // (esp_aes_decrypt_init_t)aes_decrypt_init, - .aes_decrypt_deinit = NULL, // (esp_aes_decrypt_deinit_t)aes_decrypt_deinit, - .aes_128_encrypt = NULL, // (esp_aes_128_encrypt_t)aes_128_cbc_encrypt, - .aes_128_decrypt = NULL, // (esp_aes_128_decrypt_t)aes_128_cbc_decrypt, - .omac1_aes_128 = NULL, // (esp_omac1_aes_128_t)omac1_aes_128, - .ccmp_decrypt = NULL, // (esp_ccmp_decrypt_t)ccmp_decrypt, - .ccmp_encrypt = NULL, // (esp_ccmp_encrypt_t)ccmp_encrypt, - .aes_gmac = NULL, // (esp_aes_gmac_t)esp_aes_gmac, -}; - -const mesh_crypto_funcs_t g_wifi_default_mesh_crypto_funcs = { - .aes_128_encrypt = NULL, // (esp_aes_128_encrypt_t)aes_128_cbc_encrypt, - .aes_128_decrypt = NULL, // (esp_aes_128_decrypt_t)aes_128_cbc_decrypt, -}; diff --git a/dependencies.lock b/dependencies.lock index 003204fed..e1a93f5ef 100644 --- a/dependencies.lock +++ b/dependencies.lock @@ -1,21 +1,21 @@ dependencies: espressif/esp_tinyusb: - component_hash: 28c68ff118750c0a754c4563c700953a8b54ccb7644fdffb0658e4effa3528c3 + component_hash: 68acc21eb256091ce949605bd6f5ec0ec90a7d05536c06b104de520ee7e2e146 source: service_url: https://api.components.espressif.com/ type: service - version: 1.3.0 + version: 1.4.0 espressif/tinyusb: - component_hash: 68e971ee08d20180b1a092e5ee727877a3b3d8c815fde0a91fc6fdff06d41ead + component_hash: d8d4686eb539a8c9b3689c07df119113ffc2c438606cc633c56aa335fc2e9579 source: service_url: https://api.components.espressif.com/ type: service - version: 0.14.3 + version: 0.15.0~2 idf: component_hash: null source: type: idf - version: 5.1.0 -manifest_hash: 9d292a31a2bd7dba098946f0ce92b14dd3c682c091f9ff6fc65b6519b8ef6c3d + version: 5.1.1 +manifest_hash: c303dbd840e0549311e5e896c5c6cd8b947bcfd99febcdae64b08facea506735 target: esp32s2 version: 1.0.0 diff --git a/docs/PORTING.md b/docs/PORTING.md index 2b2de2921..05b1e195c 100644 --- a/docs/PORTING.md +++ b/docs/PORTING.md @@ -77,6 +77,7 @@ _TUNERNOME_H_ | `embeddednf_data` | `embeddedNf_data` | | `embeddedout_data` | `embeddedOut_data` | | `FIXBPERO` | `FIX_B_PER_O` | +| `FIXBINS` | `FIX_BINS` | | `UP` | `PB_UP` | | `DOWN` | `PB_DOWN` | | `LEFT` | `PB_LEFT` | @@ -91,7 +92,10 @@ _TUNERNOME_H_ | `incMicGain()` | `incMicGainSetting()` | | `decMicGain()` | `decMicGainSetting()` | | `getMicGain()` | `getMicGainSetting()` | +| `setMicGain()` | `setMicGainSetting()` | | `setAndSaveLedBrightness(` | `setLedBrightnessSetting(` | +| `getTestModePassed(` | `getTestModePassedSetting(`| +| `setTestModePassed(` | `setTestModePassedSetting(`| | `meleeMenu_t` | `menu_t` | | `deinitMeleeMenu(` | `deinitMenu(` | | `modeMainMenu` | `mainMenuMode` | diff --git a/docs/SETUP.md b/docs/SETUP.md index 54b10a3b6..7aabc53d1 100644 --- a/docs/SETUP.md +++ b/docs/SETUP.md @@ -2,7 +2,7 @@ ## General Notes -It is strongly recommend that you follow the instructions on this page to set up your development environment, including the ESP-IDF. It is also possible to follow [Espressif's instructions to install ESP-IDF](https://docs.espressif.com/projects/esp-idf/en/v5.1/esp32s2/get-started/index.html#installation) through a standalone installer or an IDE. This can be done if you're sure you know what you're doing or the process written here doesn't work anymore. +It is strongly recommend that you follow the instructions on this page to set up your development environment, including the ESP-IDF. It is also possible to follow [Espressif's instructions to install ESP-IDF](https://docs.espressif.com/projects/esp-idf/en/v5.1.1/esp32s2/get-started/index.html#installation) through a standalone installer or an IDE. This can be done if you're sure you know what you're doing or the process written here doesn't work anymore. It is recommended to use native tools (i.e. Windows programs on Windows), not Windows Subsystem for Linux (WSL) or a virtual machine. @@ -49,10 +49,10 @@ The continuous integration for this project runs on a Windows instance. This mea ![image](https://user-images.githubusercontent.com/231180/224911026-0c6b1063-e4f2-4671-a804-bce004085a3a.png) -8. Clone the ESP-IDF v5.1 and install the tools. Note that it will clone into `$HOME/esp/esp-idf`. +8. Clone the ESP-IDF v5.1.1 and install the tools. Note that it will clone into `$HOME/esp/esp-idf`. ```powershell & Set-ExecutionPolicy -Scope CurrentUser Unrestricted - & git clone -b v5.1 --recurse-submodules https://github.com/espressif/esp-idf.git $HOME/esp/esp-idf + & git clone -b v5.1.1 --recurse-submodules https://github.com/espressif/esp-idf.git $HOME/esp/esp-idf & $HOME\esp\esp-idf\install.ps1 ``` > **Warning** @@ -64,16 +64,16 @@ The continuous integration for this project runs on a Windows instance. This mea 1. Run the following commands, depending on your package manager, to install all necessary packages: * `apt`: ```bash - sudo apt install build-essential xorg-dev libx11-dev libxinerama-dev libxext-dev mesa-common-dev libglu1-mesa-dev libasound2-dev libpulse-dev libasan8 clang-format cppcheck doxygen python3 python3-pip python3-venv cmake libusb-1.0-0-dev + sudo apt install build-essential xorg-dev libx11-dev libxinerama-dev libxext-dev mesa-common-dev libglu1-mesa-dev libasound2-dev libpulse-dev libasan8 clang-format cppcheck doxygen python3 python3-pip python3-venv cmake libusb-1.0-0-dev lcov ``` * `dnf`: ```bash sudo dnf group install "C Development Tools and Libraries" "Development Tools" - sudo dnf install libX11-devel libXinerama-devel libXext-devel mesa-libGLU-devel alsa-lib-devel pulseaudio-libs-devel libudev-devel cmake libasan8 clang-format cppcheck doxygen python3 python3-pip python3-venv cmake libusb-1.0-0-dev + sudo dnf install libX11-devel libXinerama-devel libXext-devel mesa-libGLU-devel alsa-lib-devel pulseaudio-libs-devel libudev-devel cmake libasan8 clang-format cppcheck doxygen python3 python3-pip python3-venv cmake libusb-1.0-0-dev lcov ``` -2. Clone the ESP-IDF v5.1 and install the tools. Note that it will clone into `~/esp/esp-idf`. +2. Clone the ESP-IDF v5.1.1 and install the tools. Note that it will clone into `~/esp/esp-idf`. ```bash - git clone -b v5.1 --recurse-submodules https://github.com/espressif/esp-idf.git ~/esp/esp-idf + git clone -b v5.1.1 --recurse-submodules https://github.com/espressif/esp-idf.git ~/esp/esp-idf ~/esp/esp-idf/install.sh ``` diff --git a/docs/doxygen-awesome-css/DoxygenLayout.xml b/docs/doxygen-awesome-css/DoxygenLayout.xml index 8a70757a4..0cc740fa4 100644 --- a/docs/doxygen-awesome-css/DoxygenLayout.xml +++ b/docs/doxygen-awesome-css/DoxygenLayout.xml @@ -1,10 +1,15 @@ + - + - + + + + + @@ -44,7 +49,7 @@ - + @@ -143,8 +148,8 @@ - - + + @@ -180,9 +185,10 @@ - + + @@ -227,6 +233,25 @@ + + + + + + + + + + + + + + + + + + + diff --git a/docs/doxygen-awesome-css/header.html b/docs/doxygen-awesome-css/header.html index fdb7e4de1..f55ee5101 100644 --- a/docs/doxygen-awesome-css/header.html +++ b/docs/doxygen-awesome-css/header.html @@ -1,4 +1,4 @@ - + diff --git a/emulator/idf-inc/class/hid/hid.h b/emulator/idf-inc/class/hid/hid.h index 452de4605..260b4c2e7 100644 --- a/emulator/idf-inc/class/hid/hid.h +++ b/emulator/idf-inc/class/hid/hid.h @@ -1,8 +1,66 @@ #pragma once -#include +#include "tinyusb.h" -#define TU_ATTR_PACKED __attribute__((packed)) +#define TUD_HID_REPORT_DESC_GAMEPAD(x) + +#define TUD_CONFIG_DESC_LEN (9) + +// Config number, interface count, string index, total length, attribute, power in mA +#define TUD_CONFIG_DESCRIPTOR(config_num, _itfcount, _stridx, _total_len, _attribute, _power_ma) \ + config_num, _itfcount, _stridx, _total_len, _attribute, _power_ma +#define TUD_HID_DESCRIPTOR(_itfnum, _stridx, _boot_protocol, _report_desc_len, _epin, _epsize, _ep_interval) \ + _itfnum, _stridx, _boot_protocol, _report_desc_len, _epin, _epsize, _ep_interval + +#define CFG_TUD_HID 1 +#define TUD_HID_DESC_LEN (9 + 9 + 7) + +enum +{ + TUSB_DESC_CONFIG_ATT_REMOTE_WAKEUP = TU_BIT(5), + TUSB_DESC_CONFIG_ATT_SELF_POWERED = TU_BIT(6), +}; + +//--------------------------------------------------------------------+ +// GAMEPAD +//--------------------------------------------------------------------+ +/** \addtogroup ClassDriver_HID_Gamepad Gamepad + * @{ */ + +/* From https://www.kernel.org/doc/html/latest/input/gamepad.html + ____________________________ __ + / [__ZL__] [__ZR__] \ | + / [__ TL __] [__ TR __] \ | Front Triggers + __/________________________________\__ __| + / _ \ | + / /\ __ (N) \ | + / || __ |MO| __ _ _ \ | Main Pad + | <===DP===> |SE| |ST| (W) -|- (E) | | + \ || ___ ___ _ / | + /\ \/ / \ / \ (S) /\ __| + / \________ | LS | ____ | RS | ________/ \ | +| / \ \___/ / \ \___/ / \ | | Control Sticks +| / \_____/ \_____/ \ | __| +| / \ | + \_____/ \_____/ + + |________|______| |______|___________| + D-Pad Left Right Action Pad + Stick Stick + + |_____________| + Menu Pad + + Most gamepads have the following features: + - Action-Pad 4 buttons in diamonds-shape (on the right side) NORTH, SOUTH, WEST and EAST. + - D-Pad (Direction-pad) 4 buttons (on the left side) that point up, down, left and right. + - Menu-Pad Different constellations, but most-times 2 buttons: SELECT - START. + - Analog-Sticks provide freely moveable sticks to control directions, Analog-sticks may also + provide a digital button if you press them. + - Triggers are located on the upper-side of the pad in vertical direction. The upper buttons + are normally named Left- and Right-Triggers, the lower buttons Z-Left and Z-Right. + - Rumble Many devices provide force-feedback features. But are mostly just simple rumble motors. + */ /// HID Gamepad Protocol Report. typedef struct TU_ATTR_PACKED @@ -16,3 +74,83 @@ typedef struct TU_ATTR_PACKED uint8_t hat; ///< Buttons mask for currently pressed buttons in the DPad/hat uint32_t buttons; ///< Buttons mask for currently pressed buttons } hid_gamepad_report_t; + +/// Standard Gamepad Buttons Bitmap +typedef enum +{ + GAMEPAD_BUTTON_0 = TU_BIT(0), + GAMEPAD_BUTTON_1 = TU_BIT(1), + GAMEPAD_BUTTON_2 = TU_BIT(2), + GAMEPAD_BUTTON_3 = TU_BIT(3), + GAMEPAD_BUTTON_4 = TU_BIT(4), + GAMEPAD_BUTTON_5 = TU_BIT(5), + GAMEPAD_BUTTON_6 = TU_BIT(6), + GAMEPAD_BUTTON_7 = TU_BIT(7), + GAMEPAD_BUTTON_8 = TU_BIT(8), + GAMEPAD_BUTTON_9 = TU_BIT(9), + GAMEPAD_BUTTON_10 = TU_BIT(10), + GAMEPAD_BUTTON_11 = TU_BIT(11), + GAMEPAD_BUTTON_12 = TU_BIT(12), + GAMEPAD_BUTTON_13 = TU_BIT(13), + GAMEPAD_BUTTON_14 = TU_BIT(14), + GAMEPAD_BUTTON_15 = TU_BIT(15), + GAMEPAD_BUTTON_16 = TU_BIT(16), + GAMEPAD_BUTTON_17 = TU_BIT(17), + GAMEPAD_BUTTON_18 = TU_BIT(18), + GAMEPAD_BUTTON_19 = TU_BIT(19), + GAMEPAD_BUTTON_20 = TU_BIT(20), + GAMEPAD_BUTTON_21 = TU_BIT(21), + GAMEPAD_BUTTON_22 = TU_BIT(22), + GAMEPAD_BUTTON_23 = TU_BIT(23), + GAMEPAD_BUTTON_24 = TU_BIT(24), + GAMEPAD_BUTTON_25 = TU_BIT(25), + GAMEPAD_BUTTON_26 = TU_BIT(26), + GAMEPAD_BUTTON_27 = TU_BIT(27), + GAMEPAD_BUTTON_28 = TU_BIT(28), + GAMEPAD_BUTTON_29 = TU_BIT(29), + GAMEPAD_BUTTON_30 = TU_BIT(30), + GAMEPAD_BUTTON_31 = TU_BIT(31), +} hid_gamepad_button_bm_t; + +/// Standard Gamepad Buttons Naming from Linux input event codes +/// https://github.com/torvalds/linux/blob/master/include/uapi/linux/input-event-codes.h +#define GAMEPAD_BUTTON_A GAMEPAD_BUTTON_0 +#define GAMEPAD_BUTTON_SOUTH GAMEPAD_BUTTON_0 + +#define GAMEPAD_BUTTON_B GAMEPAD_BUTTON_1 +#define GAMEPAD_BUTTON_EAST GAMEPAD_BUTTON_1 + +#define GAMEPAD_BUTTON_C GAMEPAD_BUTTON_2 + +#define GAMEPAD_BUTTON_X GAMEPAD_BUTTON_3 +#define GAMEPAD_BUTTON_NORTH GAMEPAD_BUTTON_3 + +#define GAMEPAD_BUTTON_Y GAMEPAD_BUTTON_4 +#define GAMEPAD_BUTTON_WEST GAMEPAD_BUTTON_4 + +#define GAMEPAD_BUTTON_Z GAMEPAD_BUTTON_5 +#define GAMEPAD_BUTTON_TL GAMEPAD_BUTTON_6 +#define GAMEPAD_BUTTON_TR GAMEPAD_BUTTON_7 +#define GAMEPAD_BUTTON_TL2 GAMEPAD_BUTTON_8 +#define GAMEPAD_BUTTON_TR2 GAMEPAD_BUTTON_9 +#define GAMEPAD_BUTTON_SELECT GAMEPAD_BUTTON_10 +#define GAMEPAD_BUTTON_START GAMEPAD_BUTTON_11 +#define GAMEPAD_BUTTON_MODE GAMEPAD_BUTTON_12 +#define GAMEPAD_BUTTON_THUMBL GAMEPAD_BUTTON_13 +#define GAMEPAD_BUTTON_THUMBR GAMEPAD_BUTTON_14 + +/// Standard Gamepad HAT/DPAD Buttons (from Linux input event codes) +typedef enum +{ + GAMEPAD_HAT_CENTERED = 0, ///< DPAD_CENTERED + GAMEPAD_HAT_UP = 1, ///< DPAD_UP + GAMEPAD_HAT_UP_RIGHT = 2, ///< DPAD_UP_RIGHT + GAMEPAD_HAT_RIGHT = 3, ///< DPAD_RIGHT + GAMEPAD_HAT_DOWN_RIGHT = 4, ///< DPAD_DOWN_RIGHT + GAMEPAD_HAT_DOWN = 5, ///< DPAD_DOWN + GAMEPAD_HAT_DOWN_LEFT = 6, ///< DPAD_DOWN_LEFT + GAMEPAD_HAT_LEFT = 7, ///< DPAD_LEFT + GAMEPAD_HAT_UP_LEFT = 8, ///< DPAD_UP_LEFT +} hid_gamepad_hat_t; + +/// @} \ No newline at end of file diff --git a/emulator/idf-inc/esp_now.h b/emulator/idf-inc/esp_now.h index 47a684ea5..dfefedb24 100644 --- a/emulator/idf-inc/esp_now.h +++ b/emulator/idf-inc/esp_now.h @@ -31,7 +31,7 @@ typedef struct signed noise_floor : 8; /**< noise floor of Radio Frequency Module(RF). unit: dBm*/ #elif defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32C3) \ || defined(CONFIG_IDF_TARGET_ESP32C2) - unsigned : 8; /**< reserved */ + unsigned : 8; /**< reserved */ #endif unsigned ampdu_cnt : 8; /**< ampdu cnt */ unsigned channel : 4; /**< primary channel on which this packet is received */ @@ -54,9 +54,9 @@ typedef struct signed noise_floor : 8; /**< noise floor of Radio Frequency Module(RF). unit: dBm*/ unsigned : 24; /**< reserved */ #elif defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32C2) - unsigned : 32; /**< reserved */ - unsigned : 32; /**< reserved */ - unsigned : 32; /**< reserved */ + unsigned : 32; /**< reserved */ + unsigned : 32; /**< reserved */ + unsigned : 32; /**< reserved */ #endif unsigned sig_len : 12; /**< length of packet including Frame Check Sequence(FCS) */ unsigned : 12; /**< reserved */ diff --git a/emulator/idf-inc/esp_wifi.h b/emulator/idf-inc/esp_wifi.h index 0f2b6e734..d51b44d6a 100644 --- a/emulator/idf-inc/esp_wifi.h +++ b/emulator/idf-inc/esp_wifi.h @@ -3,6 +3,33 @@ #include #include +#define ESP_ERR_WIFI_NOT_INIT (ESP_ERR_WIFI_BASE + 1) /*!< WiFi driver was not installed by esp_wifi_init */ +#define ESP_ERR_WIFI_NOT_STARTED (ESP_ERR_WIFI_BASE + 2) /*!< WiFi driver was not started by esp_wifi_start */ +#define ESP_ERR_WIFI_NOT_STOPPED (ESP_ERR_WIFI_BASE + 3) /*!< WiFi driver was not stopped by esp_wifi_stop */ +#define ESP_ERR_WIFI_IF (ESP_ERR_WIFI_BASE + 4) /*!< WiFi interface error */ +#define ESP_ERR_WIFI_MODE (ESP_ERR_WIFI_BASE + 5) /*!< WiFi mode error */ +#define ESP_ERR_WIFI_STATE (ESP_ERR_WIFI_BASE + 6) /*!< WiFi internal state error */ +#define ESP_ERR_WIFI_CONN (ESP_ERR_WIFI_BASE + 7) /*!< WiFi internal control block of station or soft-AP error */ +#define ESP_ERR_WIFI_NVS (ESP_ERR_WIFI_BASE + 8) /*!< WiFi internal NVS module error */ +#define ESP_ERR_WIFI_MAC (ESP_ERR_WIFI_BASE + 9) /*!< MAC address is invalid */ +#define ESP_ERR_WIFI_SSID (ESP_ERR_WIFI_BASE + 10) /*!< SSID is invalid */ +#define ESP_ERR_WIFI_PASSWORD (ESP_ERR_WIFI_BASE + 11) /*!< Password is invalid */ +#define ESP_ERR_WIFI_TIMEOUT (ESP_ERR_WIFI_BASE + 12) /*!< Timeout error */ +#define ESP_ERR_WIFI_WAKE_FAIL (ESP_ERR_WIFI_BASE + 13) /*!< WiFi is in sleep state(RF closed) and wakeup fail */ +#define ESP_ERR_WIFI_WOULD_BLOCK (ESP_ERR_WIFI_BASE + 14) /*!< The caller would block */ +#define ESP_ERR_WIFI_NOT_CONNECT (ESP_ERR_WIFI_BASE + 15) /*!< Station still in disconnect status */ + +#define ESP_ERR_WIFI_POST (ESP_ERR_WIFI_BASE + 18) /*!< Failed to post the event to WiFi task */ +#define ESP_ERR_WIFI_INIT_STATE (ESP_ERR_WIFI_BASE + 19) /*!< Invalid WiFi state when init/deinit is called */ +#define ESP_ERR_WIFI_STOP_STATE (ESP_ERR_WIFI_BASE + 20) /*!< Returned when WiFi is stopping */ +#define ESP_ERR_WIFI_NOT_ASSOC (ESP_ERR_WIFI_BASE + 21) /*!< The WiFi connection is not associated */ +#define ESP_ERR_WIFI_TX_DISALLOW (ESP_ERR_WIFI_BASE + 22) /*!< The WiFi TX is disallowed */ + +#define ESP_ERR_WIFI_TWT_FULL (ESP_ERR_WIFI_BASE + 23) /*!< no available flow id */ +#define ESP_ERR_WIFI_TWT_SETUP_TIMEOUT \ + (ESP_ERR_WIFI_BASE \ + + 24) /*!< Timeout of receiving twt setup response frame, timeout times can be set during twt setup */ + typedef enum { ESP_IF_WIFI_STA = 0, /**< Station interface */ diff --git a/emulator/idf-inc/tinyusb.h b/emulator/idf-inc/tinyusb.h new file mode 100644 index 000000000..b1b2960e0 --- /dev/null +++ b/emulator/idf-inc/tinyusb.h @@ -0,0 +1,114 @@ +#pragma once + +#include +#include +#include + +//--------------------------------------------------------------------+ +// Macros Helper +//--------------------------------------------------------------------+ +#define TU_ARRAY_SIZE(_arr) (sizeof(_arr) / sizeof(_arr[0])) +#define TU_MIN(_x, _y) (((_x) < (_y)) ? (_x) : (_y)) +#define TU_MAX(_x, _y) (((_x) > (_y)) ? (_x) : (_y)) + +#define TU_U16(_high, _low) ((uint16_t)(((_high) << 8) | (_low))) +#define TU_U16_HIGH(_u16) ((uint8_t)(((_u16) >> 8) & 0x00ff)) +#define TU_U16_LOW(_u16) ((uint8_t)((_u16) & 0x00ff)) +#define U16_TO_U8S_BE(_u16) TU_U16_HIGH(_u16), TU_U16_LOW(_u16) +#define U16_TO_U8S_LE(_u16) TU_U16_LOW(_u16), TU_U16_HIGH(_u16) + +#define TU_U32_BYTE3(_u32) ((uint8_t)((((uint32_t)_u32) >> 24) & 0x000000ff)) // MSB +#define TU_U32_BYTE2(_u32) ((uint8_t)((((uint32_t)_u32) >> 16) & 0x000000ff)) +#define TU_U32_BYTE1(_u32) ((uint8_t)((((uint32_t)_u32) >> 8) & 0x000000ff)) +#define TU_U32_BYTE0(_u32) ((uint8_t)(((uint32_t)_u32) & 0x000000ff)) // LSB + +#define U32_TO_U8S_BE(_u32) TU_U32_BYTE3(_u32), TU_U32_BYTE2(_u32), TU_U32_BYTE1(_u32), TU_U32_BYTE0(_u32) +#define U32_TO_U8S_LE(_u32) TU_U32_BYTE0(_u32), TU_U32_BYTE1(_u32), TU_U32_BYTE2(_u32), TU_U32_BYTE3(_u32) + +#define TU_BIT(n) (1UL << (n)) +#define TU_GENMASK(h, l) ((UINT32_MAX << (l)) & (UINT32_MAX >> (31 - (h)))) + +#define TU_ATTR_PACKED __attribute__((packed)) + +/// USB Device Descriptor +typedef struct TU_ATTR_PACKED +{ + uint8_t bLength; ///< Size of this descriptor in bytes. + uint8_t bDescriptorType; ///< DEVICE Descriptor Type. + uint16_t bcdUSB; ///< BUSB Specification Release Number in Binary-Coded Decimal (i.e., 2.10 is 210H). This field + ///< identifies the release of the USB Specification with which the device and its descriptors are + ///< compliant. + + uint8_t + bDeviceClass; ///< Class code (assigned by the USB-IF). \li If this field is reset to zero, each interface + ///< within a configuration specifies its own class information and the various interfaces operate + ///< independently. \li If this field is set to a value between 1 and FEH, the device supports + ///< different class specifications on different interfaces and the interfaces may not operate + ///< independently. This value identifies the class definition used for the aggregate interfaces. + ///< \li If this field is set to FFH, the device class is vendor-specific. + uint8_t bDeviceSubClass; ///< Subclass code (assigned by the USB-IF). These codes are qualified by the value of the + ///< bDeviceClass field. \li If the bDeviceClass field is reset to zero, this field must + ///< also be reset to zero. \li If the bDeviceClass field is not set to FFH, all values are + ///< reserved for assignment by the USB-IF. + uint8_t + bDeviceProtocol; ///< Protocol code (assigned by the USB-IF). These codes are qualified by the value of the + ///< bDeviceClass and the bDeviceSubClass fields. If a device supports class-specific protocols + ///< on a device basis as opposed to an interface basis, this code identifies the protocols + ///< that the device uses as defined by the specification of the device class. \li If this + ///< field is reset to zero, the device does not use class-specific protocols on a device + ///< basis. However, it may use classspecific protocols on an interface basis. \li If this + ///< field is set to FFH, the device uses a vendor-specific protocol on a device basis. + uint8_t bMaxPacketSize0; ///< Maximum packet size for endpoint zero (only 8, 16, 32, or 64 are valid). For HS + ///< devices is fixed to 64. + + uint16_t idVendor; ///< Vendor ID (assigned by the USB-IF). + uint16_t idProduct; ///< Product ID (assigned by the manufacturer). + uint16_t bcdDevice; ///< Device release number in binary-coded decimal. + uint8_t iManufacturer; ///< Index of string descriptor describing manufacturer. + uint8_t iProduct; ///< Index of string descriptor describing product. + uint8_t iSerialNumber; ///< Index of string descriptor describing the device's serial number. + + uint8_t bNumConfigurations; ///< Number of possible configurations. +} tusb_desc_device_t; + +/** + * @brief Configuration structure of the TinyUSB core + * + * USB specification mandates self-powered devices to monitor USB VBUS to detect connection/disconnection events. + * If you want to use this feature, connected VBUS to any free GPIO through a voltage divider or voltage comparator. + * The voltage divider output should be (0.75 * Vdd) if VBUS is 4.4V (lowest valid voltage at device port). + * The comparator thresholds should be set with hysteresis: 4.35V (falling edge) and 4.75V (raising edge). + */ +typedef struct +{ + union + { + const tusb_desc_device_t* + device_descriptor; /*!< Pointer to a device descriptor. If set to NULL, the TinyUSB device will use a + default device descriptor whose values are set in Kconfig */ + const tusb_desc_device_t* descriptor + __attribute__((deprecated)); /*!< Alias to `device_descriptor` for backward compatibility */ + }; + const char** string_descriptor; /*!< Pointer to array of string descriptors. If set to NULL, TinyUSB device will use + a default string descriptors whose values are set in Kconfig */ + int string_descriptor_count; /*!< Number of descriptors in above array */ + bool external_phy; /*!< Should USB use an external PHY */ + const uint8_t* + configuration_descriptor; /*!< Pointer to a configuration descriptor. If set to NULL, TinyUSB device will use a + default configuration descriptor whose values are set in Kconfig */ + bool self_powered; /*!< This is a self-powered USB device. USB VBUS must be monitored. */ + int vbus_monitor_io; /*!< GPIO for VBUS monitoring. Ignored if not self_powered. */ +} tinyusb_config_t; + +/// HID Interface Protocol +typedef enum +{ + HID_ITF_PROTOCOL_NONE = 0, ///< None + HID_ITF_PROTOCOL_KEYBOARD = 1, ///< Keyboard + HID_ITF_PROTOCOL_MOUSE = 2 ///< Mouse +} hid_interface_protocol_enum_t; + +esp_err_t tinyusb_driver_install(const tinyusb_config_t* config); +bool tud_ready(void); +bool tud_hid_gamepad_report(uint8_t report_id, int8_t x, int8_t y, int8_t z, int8_t rz, int8_t rx, int8_t ry, + uint8_t hat, uint32_t buttons); \ No newline at end of file diff --git a/emulator/src/components/hdw-btn/hdw-btn.c b/emulator/src/components/hdw-btn/hdw-btn.c index e732274d0..1bab132ce 100644 --- a/emulator/src/components/hdw-btn/hdw-btn.c +++ b/emulator/src/components/hdw-btn/hdw-btn.c @@ -124,16 +124,27 @@ bool checkButtonQueue(buttonEvt_t* evt) int getTouchJoystick(int32_t* phi, int32_t* r, int32_t* intensity) { // If lastTouchIntensity is 0, we should return false as that's "not touched" - // But still perform the null checks on the args like the real swadge first - if (!phi || !r || !intensity || 0 == lastTouchIntensity) + if (0 == lastTouchIntensity) { return false; } // A touch in the center at 50% intensity - *phi = lastTouchPhi; - *r = lastTouchRadius; - *intensity = lastTouchIntensity; + if (phi) + { + *phi = lastTouchPhi; + } + + if (r) + { + *r = lastTouchRadius; + } + + if (intensity) + { + *intensity = lastTouchIntensity; + } + return true; } diff --git a/emulator/src/components/hdw-esp-now/hdw-esp-now.c b/emulator/src/components/hdw-esp-now/hdw-esp-now.c index 182f3fff4..5090b19dc 100644 --- a/emulator/src/components/hdw-esp-now/hdw-esp-now.c +++ b/emulator/src/components/hdw-esp-now/hdw-esp-now.c @@ -2,9 +2,47 @@ // Includes //============================================================================== +#if defined(WINDOWS) || defined(WIN32) || defined(WIN64) || defined(_WIN32) || defined(_WIN64) || defined(__MINGW32__) + #define USING_WINDOWS 1 +#elif defined(__linux__) + #define USING_LINUX 1 +#else + #error "OS Not Detected" +#endif + +#if defined(USING_WINDOWS) + #include +#elif defined(USING_LINUX) + #include // for socket(), connect(), sendto(), and recvfrom() + #include // for sockaddr_in and inet_addr() + #include +#endif + +#include +#include +#include + #include "hdw-esp-now.h" +#include "esp_wifi.h" +#include "esp_log.h" #include "emu_main.h" +//============================================================================== +// Defines +//============================================================================== + +#define ESP_NOW_PORT 32888 +#define MAXRECVSTRING 1024 // Longest string to receive + +//============================================================================== +// Variables +//============================================================================== + +hostEspNowRecvCb_t hostEspNowRecvCb = NULL; +hostEspNowSendCb_t hostEspNowSendCb = NULL; + +int socketFd; + //============================================================================== // Functions //============================================================================== @@ -25,7 +63,80 @@ esp_err_t initEspNow(hostEspNowRecvCb_t recvCb, hostEspNowSendCb_t sendCb, gpio_num_t rx, gpio_num_t tx, uart_port_t uart, wifiMode_t wifiMode) { - WARN_UNIMPLEMENTED(); + // Save callbacks + hostEspNowRecvCb = recvCb; + hostEspNowSendCb = sendCb; + +#if defined(USING_WINDOWS) + // Initialize Winsock + WSADATA wsaData; + if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) + { + ESP_LOGE("WIFI", "WSAStartup failed"); + return ESP_ERR_WIFI_IF; + } +#endif + + // Create a best-effort datagram socket using UDP + if ((socketFd = socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP)) < 0) + { + ESP_LOGE("WIFI", "socket() failed"); + return ESP_ERR_WIFI_IF; + } + + // Set socket to allow broadcast + int broadcastPermission = 1; + if (setsockopt(socketFd, SOL_SOCKET, SO_BROADCAST, (void*)&broadcastPermission, sizeof(broadcastPermission)) < 0) + { + ESP_LOGE("WIFI", "setsockopt() failed"); + return ESP_ERR_WIFI_IF; + } + + // Allow multiple sockets to bind to the same port + int enable = 1; + if (setsockopt(socketFd, SOL_SOCKET, SO_REUSEADDR, (void*)&enable, sizeof(int)) < 0) + { + ESP_LOGE("WIFI", "setsockopt() failed"); + return ESP_ERR_WIFI_IF; + } + +#if defined(USING_WINDOWS) + //------------------------- + // Set the socket I/O mode: In this case FIONBIO + // enables or disables the blocking mode for the + // socket based on the numerical value of iMode. + // If iMode = 0, blocking is enabled; + // If iMode != 0, non-blocking mode is enabled. + u_long iMode = 1; + if (ioctlsocket(socketFd, FIONBIO, &iMode) != 0) + { + ESP_LOGE("WIFI", "ioctlsocket() failed"); + return ESP_ERR_WIFI_IF; + } +#else + int optval_enable = 1; + setsockopt(socketFd, SOL_SOCKET, O_NONBLOCK, (char*)&optval_enable, sizeof(optval_enable)); +#endif + + // Set nonblocking timeout + struct timeval read_timeout; + read_timeout.tv_sec = 0; + read_timeout.tv_usec = 10; + setsockopt(socketFd, SOL_SOCKET, SO_RCVTIMEO, (const char*)&read_timeout, sizeof(read_timeout)); + + // Construct bind structure + struct sockaddr_in broadcastAddr; // Broadcast Address + memset(&broadcastAddr, 0, sizeof(broadcastAddr)); // Zero out structure + broadcastAddr.sin_family = AF_INET; // Internet address family + broadcastAddr.sin_addr.s_addr = htonl(INADDR_ANY); // Any incoming interface + broadcastAddr.sin_port = htons(ESP_NOW_PORT); // Broadcast port + + // Bind to the broadcast port + if (bind(socketFd, (struct sockaddr*)&broadcastAddr, sizeof(broadcastAddr)) < 0) + { + ESP_LOGE("WIFI", "bind() failed"); + return ESP_ERR_WIFI_IF; + } return ESP_OK; } @@ -34,7 +145,7 @@ esp_err_t initEspNow(hostEspNowRecvCb_t recvCb, hostEspNowSendCb_t sendCb, gpio_ */ esp_err_t espNowUseWireless(void) { - WARN_UNIMPLEMENTED(); + // Do nothing return ESP_OK; } @@ -46,7 +157,7 @@ esp_err_t espNowUseWireless(void) */ void espNowUseSerial(bool crossoverPins) { - WARN_UNIMPLEMENTED(); + // Do nothing } /** @@ -55,7 +166,37 @@ void espNowUseSerial(bool crossoverPins) */ void checkEspNowRxQueue(void) { - WARN_UNIMPLEMENTED(); + char recvString[MAXRECVSTRING + 1]; // Buffer for received string + int recvStringLen; // Length of received string + + // While we've received a packet + while ((recvStringLen = recvfrom(socketFd, recvString, MAXRECVSTRING, 0, NULL, 0)) > 0) + { + // If the packet matches the ESP_NOW format + uint8_t recvMac[6] = {0}; + if (6 + == sscanf(recvString, "ESP_NOW-%02hhX%02hhX%02hhX%02hhX%02hhX%02hhX-", &recvMac[0], &recvMac[1], + &recvMac[2], &recvMac[3], &recvMac[4], &recvMac[5])) + { + // Make sure the MAC differs from our own + uint8_t ourMac[6] = {0}; + esp_wifi_get_mac(WIFI_IF_STA, ourMac); + if (0 != memcmp(recvMac, ourMac, sizeof(ourMac))) + { + // Set up the receive info + esp_now_recv_info_t espNowInfo = {0}; + espNowInfo.src_addr = recvMac; + espNowInfo.des_addr = ourMac; + + wifi_pkt_rx_ctrl_t packetRxCtrl = {0}; + packetRxCtrl.rssi = 0x7F; + espNowInfo.rx_ctrl = &packetRxCtrl; + + // If it does, send it to the application through the callback + hostEspNowRecvCb(&espNowInfo, (uint8_t*)&recvString[21], recvStringLen - 21, packetRxCtrl.rssi); + } + } + } } /** @@ -65,9 +206,46 @@ void checkEspNowRxQueue(void) * @param data The data to broadcast using ESP NOW * @param len The length of the data to broadcast */ -void espNowSend(const char* data, uint8_t len) +void espNowSend(const char* data, uint8_t dataLen) { - WARN_UNIMPLEMENTED(); + struct sockaddr_in broadcastAddr; // Broadcast address + + // Construct local address structure + memset(&broadcastAddr, 0, sizeof(broadcastAddr)); // Zero out structure + broadcastAddr.sin_family = AF_INET; // Internet address family + broadcastAddr.sin_addr.s_addr = htonl(INADDR_NONE); // Broadcast IP address // inet_addr("255.255.255.255"); + broadcastAddr.sin_port = htons(ESP_NOW_PORT); // Broadcast port + + // Tack on ESP-NOW header and randomized MAC address + char espNowPacket[dataLen + 24]; + uint8_t mac[6] = {0}; + esp_wifi_get_mac(WIFI_IF_STA, mac); + sprintf(espNowPacket, "ESP_NOW-%02X%02X%02X%02X%02X%02X-", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); + int hdrLen = strlen(espNowPacket); + memcpy(&espNowPacket[hdrLen], data, dataLen); + + // For the callback + uint8_t bcastMac[6] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; + + errno = 0; + // Send the packet + int sentLen + = sendto(socketFd, espNowPacket, hdrLen + dataLen, 0, (struct sockaddr*)&broadcastAddr, sizeof(broadcastAddr)); + if (sentLen != (hdrLen + dataLen)) + { + ESP_LOGE("WIFI", "sendto() sent a different number of bytes than expected: %d, not %d", sentLen, + hdrLen + dataLen); + if (errno != 0) + { + ESP_LOGE("WIFI", "errno was: %d", errno); + } + + hostEspNowSendCb(bcastMac, ESP_NOW_SEND_FAIL); + } + else + { + hostEspNowSendCb(bcastMac, ESP_NOW_SEND_SUCCESS); + } } /** @@ -75,5 +253,8 @@ void espNowSend(const char* data, uint8_t len) */ void deinitEspNow(void) { - WARN_UNIMPLEMENTED(); + close(socketFd); +#if defined(USING_WINDOWS) + WSACleanup(); +#endif } diff --git a/emulator/src/components/hdw-tft/hdw-tft.c b/emulator/src/components/hdw-tft/hdw-tft.c index d1a0f2339..3fd0bae79 100644 --- a/emulator/src/components/hdw-tft/hdw-tft.c +++ b/emulator/src/components/hdw-tft/hdw-tft.c @@ -227,7 +227,7 @@ void drawDisplayTft(fnBackgroundDrawCallback_t fnBackgroundDrawCallback) uint32_t color = paletteColorsEmu[frameBuffer[(y * TFT_WIDTH) + x]]; - uint8_t a = (color)&0xFF; + uint8_t a = (color) & 0xFF; uint8_t r = (color >> 8) & 0xFF; r = (r * tftBrightness) / CONFIG_TFT_MAX_BRIGHTNESS; uint8_t g = (color >> 16) & 0xFF; diff --git a/emulator/src/components/hdw-usb/hdw-usb.c b/emulator/src/components/hdw-usb/hdw-usb.c index be2209513..d046dd524 100644 --- a/emulator/src/components/hdw-usb/hdw-usb.c +++ b/emulator/src/components/hdw-usb/hdw-usb.c @@ -38,3 +38,14 @@ void sendUsbGamepadReport(hid_gamepad_report_t* report) { WARN_UNIMPLEMENTED(); } + +/** + * @brief Initialize TinyUSB + * + * @param tusb_cfg The TinyUSB configuration + * @param descriptor The descriptor to use for this configuration + */ +void initTusb(const tinyusb_config_t* tusb_cfg, const uint8_t* descriptor) +{ + WARN_UNIMPLEMENTED(); +} \ No newline at end of file diff --git a/emulator/src/emu_main.c b/emulator/src/emu_main.c index e9c69f6f5..90fc5482c 100644 --- a/emulator/src/emu_main.c +++ b/emulator/src/emu_main.c @@ -15,6 +15,10 @@ #include #include +#ifdef ENABLE_GCOV + #include +#endif + #include "hdw-tft.h" #include "hdw-tft_emu.h" #include "hdw-led.h" @@ -27,6 +31,9 @@ #include "macros.h" #include "trigonometry.h" +#include "hdw-esp-now.h" +#include "mainMenu.h" + // Make it so we don't need to include any other C files in our build. #define CNFG_IMPLEMENTATION #define CNFGOGL @@ -35,8 +42,6 @@ // Useful if you're trying to find the code for a key/button // #define DEBUG_INPUTS -#define EMU_EXTENSIONS - #include "emu_args.h" #include "emu_ext.h" #include "emu_main.h" @@ -45,10 +50,8 @@ // Defines //============================================================================== -#define BG_COLOR 0x191919FF // This color isn't parjt of the palette -#define DIV_WIDTH 1 -#define DIV_HEIGHT 1 -#define DIV_COLOR 0x808080FF +#define BG_COLOR 0x191919FF // This color isn't parjt of the palette +#define DIV_COLOR 0x808080FF //============================================================================== // Variables @@ -118,7 +121,6 @@ int main(int argc, char** argv) return 0; } -#ifdef EMU_EXTENSIONS // Call any init callbacks we may have and pass them the parsed command-line arguments // We also determine which extensions are enabled here, which is important for laying out the window properly initExtensions(&emulatorArgs); @@ -128,7 +130,6 @@ int main(int argc, char** argv) // One of the extension must have quit due to an error. return 0; } -#endif // First initialize rawdraw // Screen-specific configurations @@ -140,19 +141,35 @@ int main(int argc, char** argv) else { // Get all the pane info to see how much space we need aside from the simulated TFT screen - emuPaneMinimum_t paneMins[5] = {0}; - calculatePaneMinimums(&paneMins); + emuPaneMinimum_t paneMins[4] = {0}; + calculatePaneMinimums(paneMins); int32_t sidePanesW = paneMins[PANE_LEFT].min + paneMins[PANE_RIGHT].min; int32_t topBottomPanesH = paneMins[PANE_TOP].min + paneMins[PANE_BOTTOM].min; + int32_t winW = (TFT_WIDTH) * 2 + sidePanesW; + int32_t winH = (TFT_HEIGHT) * 2 + topBottomPanesH; + + if (emulatorArgs.headless) + { + // If the window dimensions are negative, the window will still exist but not be displayed. + // TODO does this work on all platforms? + winW = -winW; + winH = -winH; + } // Add the screen size to the minimum pane sizes to get our window size - CNFGSetup("Swadge 2024 Simulator", (TFT_WIDTH)*2 + sidePanesW, (TFT_HEIGHT)*2 + topBottomPanesH); + CNFGSetup("Swadge 2024 Simulator", winW, winH); } // We won't call the pre-frame callback for the very first frame // This is because everything isn't initialized and there would have to be emu-specific code to do so // post-initialization So, this is fine, we get one frame of peace before the emulator can start messing with stuff. + // This is a hack to make sure ESPNOW is always initialized on the emulator. + // The real swadge initializes wifi on boot only if the mode requires it, + // but the emulator doesn't actually reboot, so instead we just change the mode of the + // main menu mode to force ESPNOW to always initialize when the emulator starts + mainMenuMode.wifiMode = ESP_NOW; + // This is the 'main' that gets called when the ESP boots. It does not return app_main(); } @@ -163,11 +180,9 @@ int main(int argc, char** argv) */ void taskYIELD(void) { -#ifdef EMU_EXTENSIONS // Count total frames, just for callback reasons static uint64_t frameNum = 0; doExtPostFrameCb(frameNum); -#endif // Calculate time between calls static int64_t tLastCallUs = 0; @@ -204,6 +219,12 @@ void taskYIELD(void) if (!isRunning) { deinitSystem(); + CNFGTearDown(); + +#ifdef ENABLE_GCOV + __gcov_dump(); +#endif + exit(0); return; } @@ -280,10 +301,8 @@ void taskYIELD(void) CNFGBlitImage(bitmapDisplay, screenPane.paneX, screenPane.paneY, bitmapWidth, bitmapHeight); } -#ifdef EMU_EXTENSIONS // After the screen has been fully rendered, call all the render callbacks to render anything else doExtRenderCb(window_w, window_h); -#endif // Display the image and wait for time to display next frame. CNFGSwapBuffers(); @@ -296,9 +315,7 @@ void taskYIELD(void) }; nanosleep(&tSleep, &tRemaining); -#ifdef EMU_EXTENSIONS doExtPreFrameCb(++frameNum); -#endif // Below: Support for pausing and unpausing the emulator // Note: Remove the above doExtPreFrameCb()... if uncommenting the below @@ -399,13 +416,11 @@ void HandleKey(int keycode, int bDown) } #endif -#ifdef EMU_EXTENSIONS keycode = doExtKeyCb(keycode, bDown); if (keycode < 0) { return; } -#endif // Assuming no callbacks canceled the key event earlier, handle it normally emulatorHandleKeys(keycode, bDown); @@ -441,9 +456,7 @@ void HandleButton(int x, int y, int button, int bDown) printf("HandleButton(x=%d, y=%d, button=%x, bDown=%s\n", x, y, button, bDown ? "true" : "false"); #endif -#ifdef EMU_EXTENSIONS doExtMouseButtonCb(x, y, button, bDown); -#endif } /** @@ -459,9 +472,7 @@ void HandleMotion(int x, int y, int mask) printf("HandleMotion(x=%d, y=%d, mask=%x\n", x, y, mask); #endif -#ifdef EMU_EXTENSIONS doExtMouseMoveCb(x, y, mask); -#endif } /** diff --git a/emulator/src/extensions/emu_args.c b/emulator/src/extensions/emu_args.c index 464cd648b..b05868e83 100644 --- a/emulator/src/extensions/emu_args.c +++ b/emulator/src/extensions/emu_args.c @@ -89,6 +89,8 @@ emuArgs_t emulatorArgs = { .fuzzTouch = false, .fuzzMotion = false, + .headless = false, + .keymap = NULL, .lock = false, @@ -102,6 +104,12 @@ emuArgs_t emulatorArgs = { .motionDrift = false, .emulateTouch = false, + + .record = false, + .playback = false, + + .recordFile = NULL, + .replayFile = NULL, }; static const char mainDoc[] = "Emulates a swadge"; @@ -114,11 +122,15 @@ static const char argFuzz[] = "fuzz"; static const char argFuzzButtons[] = "fuzz-buttons"; static const char argFuzzTouch[] = "fuzz-touch"; static const char argFuzzMotion[] = "fuzz-motion"; +static const char argHeadless[] = "headless"; static const char argHideLeds[] = "hide-leds"; +static const char argKeymap[] = "keymap"; static const char argLock[] = "lock"; static const char argMode[] = "mode"; static const char argModeSwitch[] = "mode-switch"; static const char argModeList[] = "modes-list"; +static const char argPlayback[] = "playback"; +static const char argRecord[] = "record"; static const char argTouch[] = "touch"; static const char argHelp[] = "help"; static const char argUsage[] = "usage"; @@ -134,15 +146,18 @@ static const struct option options[] = { argFuzzButtons, optional_argument, (int*)&emulatorArgs.fuzzButtons, true }, { argFuzzTouch, optional_argument, (int*)&emulatorArgs.fuzzTouch, true }, { argFuzzMotion, optional_argument, (int*)&emulatorArgs.fuzzMotion, true }, + { argHeadless, no_argument, (int*)&emulatorArgs.headless, true }, { argHideLeds, no_argument, (int*)&emulatorArgs.hideLeds, true }, + { argKeymap, required_argument, NULL, 'k' }, { argLock, no_argument, (int*)&emulatorArgs.lock, true }, { argMode, required_argument, NULL, 'm' }, + { argPlayback, required_argument, (int*)&emulatorArgs.playback, 'p' }, + { argRecord, optional_argument, (int*)&emulatorArgs.record, 'r' }, { argModeSwitch, optional_argument, NULL, 10 }, { argModeList, no_argument, NULL, 0 }, { argTouch, no_argument, (int*)&emulatorArgs.emulateTouch, true }, { argHelp, no_argument, NULL, 'h' }, { argUsage, no_argument, NULL, 0 }, - {0}, }; @@ -156,11 +171,15 @@ static const optDoc_t argDocs[] = { 0, argFuzzButtons, "y|n", "Set whether buttons are fuzzed" }, { 0, argFuzzTouch, "y|n", "Set whether touchpad inputs are fuzzed" }, { 0, argFuzzMotion, "y|n", "Set whether motion inputs are fuzzed" }, + { 0, argHeadless, NULL, "Runs the emulator without a window." }, { 0, argHideLeds, NULL, "Don't draw simulated LEDs next to the display" }, + {'k', argKeymap, "LAYOUT", "Use an alternative keymap. LAYOUT can be azerty, colemak, or dvorak"}, {'l', argLock, NULL, "Lock the emulator in the start mode" }, {'m', argMode, "MODE", "Start the emulator in the swadge mode MODE instead of the main menu"}, { 0, argModeSwitch, "TIME", "Enable or set the timer to switch modes automatically" }, { 0, argModeList, NULL, "Print out a list of all possible values for MODE" }, + {'p', argPlayback, "FILE", "Play back recorded emulator inputs from a file" }, + {'r', argRecord, "FILE", "Record emulator inputs to a file" }, {'t', argTouch, NULL, "Simulate touch sensor readings with a virtual touchpad" }, {'h', argHelp, NULL, "Give this help list" }, { 0, argUsage, NULL, "Give a short usage message" }, @@ -182,10 +201,7 @@ static const optDoc_t argDocs[] = */ static bool handleArgument(const char* optName, const char* arg, int optVal) { - // Handle arguments with no short-option like this: - // if (optName == argUsage) - //{ doSomething(); return true } - + // Handle all arguments by their long-option, as it will always be set. if (argFuzz == optName) { // Enable Fuzz @@ -227,17 +243,22 @@ static bool handleArgument(const char* optName, const char* arg, int optVal) } return true; } - else if (argMode == optName) + else if (argKeymap == optName) { if (arg) { - emulatorArgs.startMode = arg; + emulatorArgs.keymap = arg; } + return true; + } + else if (argMode == optName) + { + emulatorArgs.startMode = arg; } else if (argModeList == optName) { int numModes; - const swadgeMode_t** modes = emulatorGetSwadgeModes(&numModes); + swadgeMode_t** modes = emulatorGetSwadgeModes(&numModes); printf("All Modes: \n"); for (int i = 0; i < numModes; i++) @@ -265,17 +286,31 @@ static bool handleArgument(const char* optName, const char* arg, int optVal) emulatorArgs.modeSwitchTime = optVal; } } + else if (argRecord == optName) + { + if (emulatorArgs.playback) + { + printf("ERR: Cannot playback and record at the same time\n"); + return false; + } - // Handle options with a short-option here: - switch (optVal) + if (arg) + { + emulatorArgs.recordFile = arg; + } + } + else if (argPlayback == optName) { - // case 'x': - // doSomething(); - // if (error) return false; - // return true; + if (emulatorArgs.record) + { + printf("ERR: Cannot playback and record at the same time\n"); + return false; + } - default: - break; + if (arg) + { + emulatorArgs.replayFile = arg; + } } // It's OK if an arg is unhandled, as it may just be a flag set automatically diff --git a/emulator/src/extensions/emu_args.h b/emulator/src/extensions/emu_args.h index dd3f93f1a..d3d982ba8 100644 --- a/emulator/src/extensions/emu_args.h +++ b/emulator/src/extensions/emu_args.h @@ -29,6 +29,8 @@ typedef struct bool fuzzTouch; bool fuzzMotion; + bool headless; + /// @brief Name of the keymap to use, or NULL if none const char* keymap; @@ -42,7 +44,23 @@ typedef struct uint16_t motionJitterAmount; bool motionDrift; + // Touch Extension + bool emulateTouch; + + // Replay Extension + + /// @brief Whether or not to record the inputs to a file + bool record; + + /// @brief Whether or not to play back recorded inputs from a file + bool playback; + + /// @brief Name of the file to record inputs to, or NULL for the default + const char* recordFile; + + /// @brief Name of the file to replay inputs from + const char* replayFile; } emuArgs_t; //============================================================================== diff --git a/emulator/src/extensions/emu_ext.c b/emulator/src/extensions/emu_ext.c index e01689c50..f05a1c7a3 100644 --- a/emulator/src/extensions/emu_ext.c +++ b/emulator/src/extensions/emu_ext.c @@ -16,7 +16,9 @@ #include "ext_touch.h" #include "ext_leds.h" #include "ext_fuzzer.h" +#include "ext_keymap.h" #include "ext_modes.h" +#include "ext_replay.h" //============================================================================== // Registered Extensions @@ -27,10 +29,8 @@ //============================================================================== static const emuExtension_t* registeredExtensions[] = { - &touchEmuCallback, - &ledEmuExtension, - &fuzzerEmuExtension, - &modesEmuExtension, + &touchEmuCallback, &ledEmuExtension, &fuzzerEmuExtension, + &keymapEmuCallback, &modesEmuExtension, &replayEmuExtension, }; //============================================================================== @@ -40,7 +40,6 @@ static const emuExtension_t* registeredExtensions[] = { #define EMU_CB_LOOP_BARE for (node_t* node = extManager.extensions.first; node != NULL; node = node->next) #define EMU_CB_INFO ((emuExtInfo_t*)(node->val)) #define EMU_CB_HAS_FN(cbFn) (EMU_CB_INFO->extension && EMU_CB_INFO->extension->cbFn) -#define EMU_CB_NAME (EMU_CB_INFO->extension->name) /** * @brief Macro to be used as a for-loop replacement for calling a particular callback @@ -114,7 +113,7 @@ static emuExtManager_t extManager = {0}; //============================================================================== static emuExtInfo_t* findExtInfo(const emuExtension_t* ext); -static emuExtension_t* findExt(const char* name); +static const emuExtension_t* findExt(const char* name); //============================================================================== // Functions @@ -156,7 +155,7 @@ static emuExtInfo_t* findExtInfo(const emuExtension_t* ext) * @param name * @return const emuExtension_t* */ -static emuExtension_t* findExt(const char* name) +static const emuExtension_t* findExt(const char* name) { const emuExtension_t** cbList = registeredExtensions; @@ -204,7 +203,7 @@ static void preloadExtensions(void) * * @param args */ -void initExtensions(const emuArgs_t* args) +void initExtensions(emuArgs_t* args) { preloadExtensions(); @@ -264,9 +263,7 @@ bool enableExtension(const char* name) emuExtInfo_t* extInfo = findExtInfo(findExt(name)); if (NULL != extInfo) { - extInfo->enabled = true; - - if (!extInfo->initialized) + if (!extInfo->initialized || !extInfo->enabled) { if (extInfo->extension->fnInitCb) { @@ -283,6 +280,8 @@ bool enableExtension(const char* name) extManager.paneMinsCalculated = false; } + extInfo->enabled = true; + return true; } @@ -474,9 +473,8 @@ void layoutPanes(int32_t winW, int32_t winH, int32_t screenW, int32_t screenH, e // Only set the pane dimensions if there are actually any panes, to avoid division-by-zero if (paneInfos[PANE_LEFT].count > 0) { - winPanes[PANE_LEFT].paneW - = MAX(0, (winW - rightDivW - leftDivW - screenPane->paneW) * (paneInfos[PANE_LEFT].min) - / (paneInfos[PANE_LEFT].min + paneInfos[PANE_RIGHT].min)); + winPanes[PANE_LEFT].paneW = (winW - rightDivW - leftDivW - screenPane->paneW) * (paneInfos[PANE_LEFT].min) + / (paneInfos[PANE_LEFT].min + paneInfos[PANE_RIGHT].min); winPanes[PANE_LEFT].paneH = winH; } @@ -488,8 +486,7 @@ void layoutPanes(int32_t winW, int32_t winH, int32_t screenW, int32_t screenH, e winPanes[PANE_RIGHT].paneY = 0; if (paneInfos[PANE_RIGHT].count > 0) { - winPanes[PANE_RIGHT].paneW - = MAX(0, winW - (winPanes[PANE_LEFT].paneW + leftDivW + screenPane->paneW + rightDivW)); + winPanes[PANE_RIGHT].paneW = winW - (winPanes[PANE_LEFT].paneW + leftDivW + screenPane->paneW + rightDivW); winPanes[PANE_RIGHT].paneH = winH; } @@ -500,8 +497,8 @@ void layoutPanes(int32_t winW, int32_t winH, int32_t screenW, int32_t screenH, e { winPanes[PANE_TOP].paneW = screenPane->paneW; // Assign the remaining space to the left and right panes proportionally with their minimum sizes - winPanes[PANE_TOP].paneH = MAX(0, (winH - bottomDivH - topDivH - screenPane->paneH) * (paneInfos[PANE_TOP].min) - / (paneInfos[PANE_TOP].min + paneInfos[PANE_BOTTOM].min)); + winPanes[PANE_TOP].paneH = (winH - bottomDivH - topDivH - screenPane->paneH) * (paneInfos[PANE_TOP].min) + / (paneInfos[PANE_TOP].min + paneInfos[PANE_BOTTOM].min); } // For the bottom one, flip things around just a bit so we can center the screen properly @@ -509,8 +506,7 @@ void layoutPanes(int32_t winW, int32_t winH, int32_t screenW, int32_t screenH, e { winPanes[PANE_BOTTOM].paneW = screenPane->paneW; // Assign whatever space is left to the right pane to account for roundoff - winPanes[PANE_BOTTOM].paneH - = MAX(0, winH - (winPanes[PANE_TOP].paneH + topDivH + screenPane->paneH + bottomDivH)); + winPanes[PANE_BOTTOM].paneH = winH - (winPanes[PANE_TOP].paneH + topDivH + screenPane->paneH + bottomDivH); } // The screen will be just below the top pane and its divider, plus half of any extra space not used by the panes diff --git a/emulator/src/extensions/emu_ext.h b/emulator/src/extensions/emu_ext.h index e242a798d..0482daf3a 100644 --- a/emulator/src/extensions/emu_ext.h +++ b/emulator/src/extensions/emu_ext.h @@ -243,7 +243,7 @@ typedef struct // Function Prototypes //============================================================================== -void initExtensions(const emuArgs_t* args); +void initExtensions(emuArgs_t* args); void deinitExtensions(void); bool enableExtension(const char* name); bool disableExtension(const char* name); diff --git a/emulator/src/extensions/fuzzer/ext_fuzzer.c b/emulator/src/extensions/fuzzer/ext_fuzzer.c index 8b914eb3e..5c81c5033 100644 --- a/emulator/src/extensions/fuzzer/ext_fuzzer.c +++ b/emulator/src/extensions/fuzzer/ext_fuzzer.c @@ -14,7 +14,7 @@ // Function Prototypes //============================================================================== -static bool fuzzerInitCb(const emuArgs_t* emuArgs); +static bool fuzzerInitCb(emuArgs_t* emuArgs); static void fuzzerPreFrameCb(uint64_t frame); //============================================================================== @@ -49,7 +49,7 @@ static fuzzer_t fuzzer = {0}; // Functions //============================================================================== -static bool fuzzerInitCb(const emuArgs_t* emuArgs) +static bool fuzzerInitCb(emuArgs_t* emuArgs) { // Save the options in our own struct for convenience fuzzer.buttons = emuArgs->fuzzButtons; diff --git a/emulator/src/extensions/keymap/ext_keymap.c b/emulator/src/extensions/keymap/ext_keymap.c new file mode 100644 index 000000000..194adcc60 --- /dev/null +++ b/emulator/src/extensions/keymap/ext_keymap.c @@ -0,0 +1,134 @@ +#include "ext_keymap.h" + +#include +#include + +#include "macros.h" + +//============================================================================== +// Function Prototypes +//============================================================================== + +static bool keymapInit(emuArgs_t* emuArgs); +static int32_t keymapKeyCb(uint32_t keycode, bool down); + +//============================================================================== +// Structs +//============================================================================== + +typedef struct +{ + const char* name; ///< The name of the keyboard layout + + union + { + /// @brief Holds each keycode in a separate char + struct + { + char up; + char down; + char left; + char right; + + char a; + char b; + + char start; + char select; + }; + + /// @brief Holds all keycodes in a single string + // These keycodes will be in the same memory + char keymap[9]; + }; +} emuKeymap_t; + +//============================================================================== +// Variables +//============================================================================== + +const emuExtension_t keymapEmuCallback = { + .name = "keymap", + .fnInitCb = keymapInit, + .fnPreFrameCb = NULL, + .fnPostFrameCb = NULL, + .fnKeyCb = keymapKeyCb, + .fnMouseMoveCb = NULL, + .fnMouseButtonCb = NULL, + .fnRenderCb = NULL, +}; + +static const emuKeymap_t keymaps[] = { + {.name = "qwerty", .keymap = "WSADLKOI"}, // QWERTY (default) + {.name = "azerty", .keymap = "ZSQDLKOI"}, // AZERTY + {.name = "colemak", .keymap = "WRASIEYU"}, // Colemak + {.name = "dvorak", .keymap = ",OAENTRC"}, // Dvorak +}; + +static const emuKeymap_t* activeKeymap = NULL; + +//============================================================================== +// Functions +//============================================================================== + +static bool keymapInit(emuArgs_t* emuArgs) +{ + if (emuArgs->keymap != NULL) + { + for (const emuKeymap_t* keymap = keymaps; keymap < (keymaps + ARRAY_SIZE(keymaps)); keymap++) + { + if (!strncmp(emuArgs->keymap, keymap->name, strlen(emuArgs->keymap))) + { + printf("Set keymap to '%s'\n", keymap->name); + // Set the keyboard map, we found it! + activeKeymap = keymap; + + // Setup successful + return true; + } + } + + if (activeKeymap == NULL) + { + // We never set a keymap + fprintf(stderr, "WARN: Unknown keyboard layout '%s'\n", emuArgs->keymap); + return false; + } + } + + // No configuration found, no need to set up + return false; +} + +static int32_t keymapKeyCb(uint32_t keycode, bool down) +{ + // Convert lowercase characters to their uppercase equivalents + if ('a' <= keycode && keycode <= 'z') + { + keycode = (keycode - 'a' + 'A'); + } + + // Check if the key matches one in the layout + if (activeKeymap != NULL) + { + for (uint8_t i = 0; i < 8; i++) + { + if (activeKeymap->keymap[i] == keycode) + { + // Remap onto qwerty + return keymaps->keymap[i]; + } + } + } + + for (uint8_t i = 0; i < 8; i++) + { + if (keymaps->keymap[i] == keycode) + { + // Stop the original key from colliding + return -1; + } + } + + return 0; +} diff --git a/emulator/src/extensions/keymap/ext_keymap.h b/emulator/src/extensions/keymap/ext_keymap.h new file mode 100644 index 000000000..b5cab68fc --- /dev/null +++ b/emulator/src/extensions/keymap/ext_keymap.h @@ -0,0 +1,20 @@ +/** + * @file emu_keymap.h + * @author dylwhich (dylan@whichard.com) + * @brief This file contains an extension that remaps keybinds for alternate layouts + * @date 2023-08-05 + * + */ +#pragma once + +//============================================================================== +// Includes +//============================================================================== + +#include "emu_ext.h" + +//============================================================================== +// Variables +//============================================================================== + +const extern emuExtension_t keymapEmuCallback; diff --git a/emulator/src/extensions/modes/ext_modes.c b/emulator/src/extensions/modes/ext_modes.c index b275d83f6..508b0ae97 100644 --- a/emulator/src/extensions/modes/ext_modes.c +++ b/emulator/src/extensions/modes/ext_modes.c @@ -17,12 +17,21 @@ | grep -v quickSettings | awk '{printf "#include \"%s\"\n",$1 }' | sort */ #include "accelTest.h" +#include "breakout.h" #include "colorchord.h" #include "dance.h" #include "demoMode.h" +#include "gamepad.h" #include "jukebox.h" +#include "lumberjack.h" #include "mainMenu.h" +#include "marbles.h" +#include "mode_paint.h" +#include "mode_ray.h" +#include "paint_share.h" #include "pong.h" +#include "pushy.h" +#include "soko.h" #include "touchTest.h" #include "tunernome.h" @@ -52,12 +61,20 @@ static swadgeMode_t* getRandomSwadgeMode(void); // clang-format off static swadgeMode_t* allSwadgeModes[] = { &accelTestMode, + &breakoutMode, &colorchordMode, &danceMode, &demoMode, + &gamepadMode, &jukeboxMode, + &lumberjackMode, &mainMenuMode, + &marblesMode, + &modePaint, &pongMode, + &pushyMode, + &rayMode, + &sokoMode, &touchTestMode, &tunernomeMode, }; @@ -161,3 +178,18 @@ swadgeMode_t* getRandomSwadgeMode(void) { return allSwadgeModes[rand() % ARRAY_SIZE(allSwadgeModes)]; } + +bool emulatorSetSwadgeModeByName(const char* name) +{ + swadgeMode_t* mode = emulatorFindSwadgeMode(name); + + if (NULL != mode) + { + emulatorForceSwitchToSwadgeMode(mode); + return true; + } + else + { + return false; + } +} \ No newline at end of file diff --git a/emulator/src/extensions/modes/ext_modes.h b/emulator/src/extensions/modes/ext_modes.h index f91ed601d..b77c2e05d 100644 --- a/emulator/src/extensions/modes/ext_modes.h +++ b/emulator/src/extensions/modes/ext_modes.h @@ -8,3 +8,4 @@ extern emuExtension_t modesEmuExtension; swadgeMode_t** emulatorGetSwadgeModes(int* count); swadgeMode_t* emulatorFindSwadgeMode(const char* name); +bool emulatorSetSwadgeModeByName(const char* name); diff --git a/emulator/src/extensions/replay/ext_replay.c b/emulator/src/extensions/replay/ext_replay.c new file mode 100644 index 000000000..1ccf20111 --- /dev/null +++ b/emulator/src/extensions/replay/ext_replay.c @@ -0,0 +1,868 @@ +#include "ext_replay.h" +#include "emu_ext.h" +#include "esp_timer.h" +#include "hdw-btn.h" +#include "hdw-btn_emu.h" +#include "hdw-accel.h" +#include "hdw-accel_emu.h" +#include "macros.h" +#include "emu_main.h" +#include "ext_modes.h" + +#include +#include +#include +#include +#include + +#include "hdw-tft_emu.h" + +//============================================================================== +// Defines +//============================================================================== + +#define HEADER "Time,Type,Value\n" + +#ifdef DEBUG + #define REPLAY_DEBUG(str, ...) printf(str "\n", __VA_ARGS__); +#else + #define REPLAY_DEBUG(str, ...) +#endif + +//============================================================================== +// Enums +//============================================================================== + +typedef enum +{ + RECORD, + REPLAY, +} replayMode_t; + +typedef enum +{ + BUTTON_PRESS, + BUTTON_RELEASE, + TOUCH_PHI, + TOUCH_R, + TOUCH_INTENSITY, + ACCEL_X, + ACCEL_Y, + ACCEL_Z, + FUZZ, + QUIT, + SCREENSHOT, + SET_MODE, +} replayLogType_t; + +#define LAST_TYPE SET_MODE + +//============================================================================== +// Structs +//============================================================================== + +typedef struct +{ + int64_t time; + + replayLogType_t type; + + union + { + buttonBit_t buttonVal; + int32_t touchVal; + int16_t accelVal; + char* filename; + char* modeName; + }; +} replayEntry_t; + +typedef struct +{ + FILE* file; + bool readCompleted; + + replayMode_t mode; + bool headerHandled; + + buttonBit_t lastButtons; + + int32_t lastTouchPhi; + int32_t lastTouchR; + int32_t lastTouchIntensity; + + int16_t lastAccelX; + int16_t lastAccelY; + int16_t lastAccelZ; + + replayEntry_t nextEntry; +} replay_t; + +//============================================================================== +// Function Prototypes +//============================================================================== + +static bool replayInit(emuArgs_t* emuArgs); +static void replayRecordFrame(uint64_t frame); +static void replayPlaybackFrame(uint64_t frame); +static void replayPreFrame(uint64_t frame); + +static bool readEntry(replayEntry_t* out); +static void writeEntry(const replayEntry_t* entry); +static void writeLe(uint8_t* vals, uint32_t size, FILE* stream); + +//============================================================================== +// Variables +//============================================================================== + +static const char* replayLogTypeStrs[] = { + "BtnDown", "BtnUp", "TouchPhi", "TouchR", "TouchI", "AccelX", + "AccelY", "AccelZ", "Fuzz", "Quit", "Screenshot", "SetMode", +}; + +static const char* replayButtonNames[] = { + "Up", "Down", "Left", "Right", "A", "B", "Start", "Select", +}; + +emuExtension_t replayEmuExtension = { + .name = "replay", + .fnInitCb = replayInit, + .fnPreFrameCb = replayPreFrame, + .fnPostFrameCb = NULL, + .fnKeyCb = NULL, + .fnMouseMoveCb = NULL, + .fnMouseButtonCb = NULL, + .fnRenderCb = NULL, +}; + +replay_t replay = {0}; + +//============================================================================== +// Functions +//============================================================================== + +/** + * @brief Initialize the replay extension + * + * @param emuArgs + * @return true If the extension is enabled and will etiher record or play back. + * @return false + */ +static bool replayInit(emuArgs_t* emuArgs) +{ + if (emuArgs->record) + { + // Construct a timestamp-based filename + struct timespec ts; + char filename[64]; + clock_gettime(CLOCK_REALTIME, &ts); + uint64_t timeSec = (uint64_t)ts.tv_sec; + snprintf(filename, sizeof(filename) - 1, "rec-%" PRIu64 ".csv", timeSec); + + // If specified, use custom filename, otherwise use timestamp one + printf("\nReplay: Recording inputs to file %s\n", emuArgs->recordFile ? emuArgs->recordFile : filename); + replay.file = fopen(emuArgs->recordFile ? emuArgs->recordFile : filename, "w"); + replay.mode = RECORD; + return replay.file != NULL; + } + else if (emuArgs->playback) + { + printf("\nReplay: Replaying inputs from file %s\n", emuArgs->replayFile); + replay.file = fopen(emuArgs->replayFile, "r"); + replay.mode = REPLAY; + + // Return true if the file was opened OK and has a valid header and first entry + return NULL != replay.file && readEntry(&replay.nextEntry); + } + + return false; +} + +static void replayRecordFrame(uint64_t frame) +{ + replayEntry_t logEntry = {0}; + + if (!replay.headerHandled) + { + replay.headerHandled = true; + fwrite(HEADER, 1, strlen(HEADER), replay.file); + } + + logEntry.time = esp_timer_get_time(); + + int32_t touchPhi, touchR, touchIntensity; + if (!getTouchJoystick(&touchPhi, &touchR, &touchIntensity)) + { + touchPhi = 0; + touchR = 0; + touchIntensity = 0; + } + + int16_t accelX, accelY, accelZ; + accelGetAccelVec(&accelX, &accelY, &accelZ); + + buttonBit_t curButtons = emulatorGetButtonState(); + + for (replayLogType_t type = BUTTON_PRESS; type <= LAST_TYPE; type += 1) + { + logEntry.type = type; + + switch (type) + { + case BUTTON_PRESS: + { + for (uint8_t i = 0; i < 8; i++) + { + buttonBit_t btn = (1 << i); + if ((curButtons & btn) != (replay.lastButtons & btn)) + { + bool press = (curButtons & btn) == btn; + logEntry.type = press ? BUTTON_PRESS : BUTTON_RELEASE; + logEntry.buttonVal = btn; + writeEntry(&logEntry); + + if (press) + { + replay.lastButtons |= btn; + } + else + { + replay.lastButtons &= ~btn; + } + } + } + break; + } + + case BUTTON_RELEASE: + // Handled already by BUTTON_PRESS for fastness + break; + + case TOUCH_PHI: + { + if (touchPhi != replay.lastTouchPhi) + { + logEntry.accelVal = touchPhi; + writeEntry(&logEntry); + } + break; + } + + case TOUCH_R: + { + if (touchR != replay.lastTouchR) + { + logEntry.touchVal = touchR; + writeEntry(&logEntry); + } + break; + } + + case TOUCH_INTENSITY: + { + if (touchIntensity != replay.lastTouchIntensity) + { + logEntry.touchVal = touchIntensity; + writeEntry(&logEntry); + } + break; + } + + case ACCEL_X: + { + if (accelX != replay.lastAccelX) + { + logEntry.accelVal = accelX; + writeEntry(&logEntry); + } + break; + } + + case ACCEL_Y: + { + if (accelY != replay.lastAccelY) + { + logEntry.accelVal = accelY; + writeEntry(&logEntry); + } + break; + } + + case ACCEL_Z: + { + if (accelZ != replay.lastAccelZ) + { + logEntry.accelVal = accelZ; + writeEntry(&logEntry); + } + break; + } + + // These would be manually inserted, so no need to handle writing + case FUZZ: + case QUIT: + case SCREENSHOT: + case SET_MODE: + break; + } + } + + replay.lastTouchR = touchR; + replay.lastTouchPhi = touchPhi; + replay.lastTouchIntensity = touchIntensity; + replay.lastAccelX = accelX; + replay.lastAccelY = accelY; + replay.lastAccelZ = accelZ; + + // Flush all the entries to the file so that we can close the file the proper way + // which is obviously to let the OS deal with it when the process exits + fflush(replay.file); +} + +/** + * @brief Play back any recorded actions queued for the given frame + * + * @param frame + */ +static void replayPlaybackFrame(uint64_t frame) +{ + // Unless we've finished reading the file completely + if (!replay.readCompleted) + { + int64_t time = esp_timer_get_time(); + int32_t touchPhi = replay.lastTouchPhi; + int32_t touchR = replay.lastTouchR; + int32_t touchIntensity = replay.lastTouchIntensity; + + int16_t accelX = replay.lastAccelX; + int16_t accelY = replay.lastAccelY; + int16_t accelZ = replay.lastAccelZ; + + while (time >= replay.nextEntry.time) + { + switch (replay.nextEntry.type) + { + case BUTTON_PRESS: + { + replay.lastButtons |= replay.nextEntry.buttonVal; + REPLAY_DEBUG("Injecting button %x down\n", replay.nextEntry.buttonVal); + emulatorInjectButton(replay.nextEntry.buttonVal, true); + break; + } + + case BUTTON_RELEASE: + { + replay.lastButtons &= (~replay.nextEntry.buttonVal); + REPLAY_DEBUG("Injecting button %x up\n", replay.nextEntry.buttonVal); + emulatorInjectButton(replay.nextEntry.buttonVal, false); + break; + } + + case TOUCH_PHI: + { + touchPhi = replay.nextEntry.touchVal; + break; + } + + case TOUCH_R: + { + touchR = replay.nextEntry.touchVal; + break; + } + + case TOUCH_INTENSITY: + { + touchIntensity = replay.nextEntry.touchVal; + break; + } + + case ACCEL_X: + { + accelX = replay.nextEntry.accelVal; + break; + } + + case ACCEL_Y: + { + accelY = replay.nextEntry.accelVal; + break; + } + + case ACCEL_Z: + { + accelZ = replay.nextEntry.accelVal; + break; + } + + case FUZZ: + { + emulatorArgs.fuzz = true; + emulatorArgs.fuzzButtons = true; + emulatorArgs.fuzzTouch = true; + emulatorArgs.fuzzMotion = true; + + printf("Replay: Enabling Fuzzer"); + enableExtension("fuzzer"); + break; + } + + case QUIT: + { + printf("Replay: Stopping Emulator\n"); + emulatorQuit(); + break; + } + + case SCREENSHOT: + { + if (NULL != replay.nextEntry.filename) + { + printf("Replay: Saving screenshot to '%s'\n", replay.nextEntry.filename); + // Screenshot has a specific name, save it to that + takeScreenshot(replay.nextEntry.filename); + + // This string is dynamically alloated, so delete it + free(replay.nextEntry.filename); + replay.nextEntry.filename = NULL; + } + else + { + // No filename was given, save it to a timestamp-based name + struct timespec ts; + char filename[64]; + clock_gettime(CLOCK_REALTIME, &ts); + + // Turns out time_t doesn't printf well, so stick it in something that does + uint64_t timeSec = (uint64_t)ts.tv_sec; + snprintf(filename, sizeof(filename) - 1, "screenshot-%" PRIu64 ".bmp", timeSec); + + printf("Replay: Saving screenshot to '%s'\n", filename); + takeScreenshot(filename); + } + break; + } + + case SET_MODE: + { + if (NULL != replay.nextEntry.modeName) + { + if (emulatorSetSwadgeModeByName(replay.nextEntry.modeName)) + { + printf("Replay: Set mode to '%s'\n", replay.nextEntry.modeName); + } + else + { + printf("ERR: Replay: Can't find mode '%s'!", replay.nextEntry.modeName); + } + + // This string is dynamically alloated, so delete it + free(replay.nextEntry.modeName); + replay.nextEntry.modeName = NULL; + } + break; + } + } + + // Get the next entry + if (!readEntry(&replay.nextEntry)) + { + printf("Replay: Reached end of recording\n"); + replay.readCompleted = true; + break; + } + } + + if (touchPhi != replay.lastTouchPhi || touchR != replay.lastTouchR + || touchIntensity != replay.lastTouchIntensity) + { + REPLAY_DEBUG("Updating touch to Phi=%d, R=%d, Intensity=%d\n", touchPhi, touchR, touchIntensity); + emulatorSetTouchJoystick(touchPhi, touchR, touchIntensity); + replay.lastTouchPhi = touchPhi; + replay.lastTouchR = touchR; + replay.lastTouchIntensity = touchIntensity; + } + + if (accelX != replay.lastAccelX || accelY != replay.lastAccelY || accelZ != replay.lastAccelZ) + { + REPLAY_DEBUG("Updating accel to X=%d, Y=%d, Z=%d\n", accelX, accelY, accelZ); + emulatorSetAccelerometer(accelX, accelY, accelZ); + replay.lastAccelX = accelX; + replay.lastAccelY = accelY; + replay.lastAccelZ = accelZ; + } + } +} + +static void replayPreFrame(uint64_t frame) +{ + switch (replay.mode) + { + case RECORD: + { + replayRecordFrame(frame); + break; + } + + case REPLAY: + { + replayPlaybackFrame(frame); + break; + } + } +} + +static bool readEntry(replayEntry_t* entry) +{ + char buffer[64]; + if (!replay.headerHandled) + { + if (1 != fscanf(replay.file, "%63[^\n]\n", buffer) || strncmp(buffer, HEADER, strlen(buffer))) + { + // Couldn't read, anything. + printf("ERR: Invalid playback file, could not parse header\n"); + return false; + } + + replay.headerHandled = true; + } + + int result; + // Read timestamp index + result = fscanf(replay.file, "%" PRId64 ",", &replay.nextEntry.time); + + // Check if the index key was readable + if (result != 1) + { + if (EOF == result) + { + // EOF returned; return false without printing an error + return false; + } + else + { + printf("ERR: Can't read Time from recording: %d\n", result); + } + return false; + } + + if (1 != fscanf(replay.file, "%63[^,],", buffer)) + { + printf("ERR: Can't read action type\n"); + return false; + } + + for (replayLogType_t type = BUTTON_PRESS; type <= LAST_TYPE; type += 1) + { + const char* str = replayLogTypeStrs[type]; + if (!strncmp(str, buffer, sizeof(buffer) - 1)) + { + replay.nextEntry.type = type; + break; + } + + if (type == LAST_TYPE) + { + printf("ERR: No action type matched '%s'\n", buffer); + return false; + // not found + } + } + + switch (replay.nextEntry.type) + { + case BUTTON_PRESS: + case BUTTON_RELEASE: + { + if (1 != fscanf(replay.file, "%63s\n", buffer)) + { + printf("ERR: Can't read button name\n"); + return false; + } + + for (uint8_t i = 0; i < 8; i++) + { + buttonBit_t button = (1 << i); + if (!strncmp(replayButtonNames[i], buffer, sizeof(buffer) - 1)) + { + replay.nextEntry.buttonVal = button; + break; + } + + if (i == 7) + { + // Should have broken by now, throw error + printf("ERR: Can't find button matching '%s'\n", buffer); + return false; + } + } + + break; + } + + case TOUCH_PHI: + case TOUCH_R: + case TOUCH_INTENSITY: + { + if (1 != fscanf(replay.file, "%" PRId32 "\n", &replay.nextEntry.touchVal)) + { + return false; + } + break; + } + + case ACCEL_X: + case ACCEL_Y: + case ACCEL_Z: + { + if (1 != fscanf(replay.file, "%hd\n", &replay.nextEntry.accelVal)) + { + return false; + } + break; + } + + case FUZZ: + { + // Just advance to the next line + fscanf(replay.file, "%*[^\n]\n"); + + break; + } + + case QUIT: + { + // Just advance to the next line + while (fgetc(replay.file) != '\n') + ; + break; + } + + case SCREENSHOT: + { + // Read the filename from the screenshot + if (1 != fscanf(replay.file, "%63[^\n]\n", buffer)) + { + // Skip to the end + while (fgetc(replay.file) != '\n') + ; + entry->filename = NULL; + } + else + { + char* tmpStr = malloc(strlen(buffer) + 1); + strncpy(tmpStr, buffer, strlen(buffer) + 1); + entry->filename = tmpStr; + } + + break; + } + + case SET_MODE: + { + // Read the mode name from the file + if (1 != fscanf(replay.file, "%63[^\n]\n", buffer)) + { + return false; + } + + char* tmpStr = malloc(strlen(buffer) + 1); + strncpy(tmpStr, buffer, strlen(buffer) + 1); + entry->modeName = tmpStr; + + break; + } + + default: + { + return false; + } + } + + return true; +} + +static void writeEntry(const replayEntry_t* entry) +{ + char buffer[256]; + char* ptr = buffer; +#define BUFSIZE (buffer + sizeof(buffer) - 1 - ptr) + + // Write time key + ptr += snprintf(ptr, BUFSIZE, "%" PRId64 ",", entry->time); + + // Write entry type + ptr += snprintf(ptr, BUFSIZE, "%s,", replayLogTypeStrs[entry->type]); + + switch (entry->type) + { + case BUTTON_PRESS: + case BUTTON_RELEASE: + { + // Find button index + int i = 0; + while ((1 << i) != entry->buttonVal && i < 7) + { + i++; + } + + // TODO: Check for invalid button index? + snprintf(ptr, BUFSIZE, "%s\n", replayButtonNames[i]); + break; + } + + case TOUCH_PHI: + case TOUCH_R: + case TOUCH_INTENSITY: + { + snprintf(ptr, BUFSIZE, "%" PRId32 "\n", entry->touchVal); + break; + } + + case ACCEL_X: + case ACCEL_Y: + case ACCEL_Z: + { + snprintf(ptr, BUFSIZE, "%" PRId16 "\n", entry->accelVal); + break; + } + + case FUZZ: + case QUIT: + case SCREENSHOT: + case SET_MODE: + { + break; + } + } + + fwrite(buffer, 1, strlen(buffer), replay.file); +} + +static void writeLe(uint8_t* vals, uint32_t size, FILE* stream) +{ + static const uint32_t test = 0x01020304; + + for (uint32_t i = 0; i < size; i++) + { + if (*((const char*)&test) == 0x04) + { + // Little Endian + fputc(vals[i], stream); + } + else + { + // Big Endian + fputc(vals[size - i - 1], stream); + } + } +} + +bool takeScreenshot(const char* name) +{ + uint16_t width, height; + uint32_t* bitmap = getDisplayBitmap(&width, &height); + + FILE* bmp = fopen(name, "wb"); + + if (!bmp) + { + printf("ERR: Unable to open file '%s' for writing\n", name); + return false; + } + +#define BMP_HEADER_SIZE 54 +#define BITS_PER_PIXEL 24 + // Calculate row size accounting for padding + uint16_t rowSize = (width * BITS_PER_PIXEL + 31) / 32 * 4; + uint16_t paddingBytesPerRow = ((width * BITS_PER_PIXEL % 32) + 7) / 8; + uint32_t pxDataSize = rowSize * height; + uint32_t totalSize = pxDataSize + BMP_HEADER_SIZE; + + uint32_t tmp32; + uint16_t tmp16; + +#define WRITE_32(x) \ + do \ + { \ + tmp32 = (x); \ + writeLe((uint8_t*)&tmp32, 4, bmp); \ + } while (0) +#define WRITE_16(x) \ + do \ + { \ + tmp16 = (x); \ + writeLe((uint8_t*)&tmp16, 2, bmp); \ + } while (0) + + // Write bitmap header + fputc('B', bmp); + fputc('M', bmp); + + // Write total size (little-endian) + WRITE_32(totalSize); + + // Write 4 Reserved Bytes + WRITE_32(0); + + // Write pixel data offset + WRITE_32(BMP_HEADER_SIZE); + + // DIB Header + // Write DIB length + WRITE_32(40); + + // Write Pixel Width + WRITE_32(width); + + // Write Pixel Height + WRITE_32(height); + + // Write color planes + WRITE_16(1); + + // Write bits per pixel + WRITE_16(24); + + // Write pixel format / compression + WRITE_32(0); + + // Write pixel data size + WRITE_32(pxDataSize); + + // Write print resolution (2853px/meter == 72DPI) + WRITE_32(2835); + WRITE_32(2853); + + // Write color palette count + WRITE_32(0); + + // Write important color count + WRITE_32(0); + + // Write the bitmap lines, from the bottom-up + for (int16_t row = height - 1; row >= 0; --row) + { + // Write the pixels in this line, from left-to-right + for (uint16_t col = 0; col < width; col++) + { + // 24BPP / 8BPC + uint8_t r = (bitmap[row * width + col] >> 8) & 0xFF; + uint8_t g = (bitmap[row * width + col] >> 16) & 0xFF; + uint8_t b = (bitmap[row * width + col] >> 24) & 0xFF; + + fputc(r, bmp); + fputc(g, bmp); + fputc(b, bmp); + } + + // Add padding at end of line + for (uint16_t i = 0; i < paddingBytesPerRow; i++) + { + fputc(0, bmp); + } + } + + fclose(bmp); + + return true; +} diff --git a/emulator/src/extensions/replay/ext_replay.h b/emulator/src/extensions/replay/ext_replay.h new file mode 100644 index 000000000..0eebb2046 --- /dev/null +++ b/emulator/src/extensions/replay/ext_replay.h @@ -0,0 +1,68 @@ +/*! \file ext_replay.h + * + * \section ext_replay Replay Emulator Extension + * + * The replay extension allows you to record swadge inputs -- button presses, touchpad inputs, + * and accelerometer readings -- to a file, and then play them back later. And when playing back + * inputs, several special actions are also supported: starting fuzzing, taking a screenshot, + * changing modes, and quitting the emulator. These must be manually added to a recording file, + * but they can used to automatically target fuzzing to a specific screen, or to automatically + * generate a screenshot of a specific screen. + * + * \section ext_format Recording File Format + * If not given a custom name, recording files will be created in the current directory with the + * name 'rec-TIMESTAMP.csv' As suggested by the name, this is simply a CSV file. The first line + * contains the header, which specifies three columns: Time, Type, and Value. The rest of the lines + * will be the individual input values or special actions. + * + * The first column, `Time`, is the timestamp, in microseconds, of the input. The next column, + * `Type`, is the type of data or the special action to perform. Possible options are: `BtnDown`, + * `BtnUp`, `TouchPhi`, `TouchR`, `TouchI`, `AccelX`, `AccelY`, or `AccelZ`, and `Screenshot`, + * `Fuzz`, `SetMode`, and `Quit`. + * + * The third column, `Value`, depends on the value of the `Type` column. For `BtnDown` and `BtnUp`, + * this is the name of the button: `A`, `B`, `Up`, `Down`, `Left`, `Right`, `Select`, or `Start`. + * For all `Touch*` and `Accel*` types, the value is an integer. For `Screenshot`, the third column + * is the filename for the screenshot, or it may be left blank for an automatically generated filename. + * For `SetMode`, the value is the name of the mode to switch to. And, for `Quit` and `Fuzz`, the third + * column is completely ignored. + * + * The following example recording file will generate a few button presses and touch events, then after + * about 4 seconds, switch to Pong, take a screenshot, begin fuzzing until 10 seconds have passed, and + * finally take another screenshot before closing the emulator. + * \code{.csv} + * Time,Type,Value + * 23558,AccelZ,242 + * 500383,BtnDown,A + * 565291,BtnUp,A + * 1498312,TouchPhi,319 + * 1498312,TouchR,132 + * 1498312,TouchI,1024 + * 1532527,TouchPhi,328 + * 1532527,TouchR,146 + * 1582694,TouchPhi,12 + * 1582694,TouchR,350 + * 1749475,TouchPhi,0 + * 1749475,TouchR,0 + * 1749475,TouchI,0 + * 2582065,BtnDown,Up + * 2631439,BtnUp,Up + * 2832071,BtnDown,Down + * 2882992,BtnUp,Down + * 3518793,BtnDown,Right + * 3568577,BtnUp,Right + * 3636235,BtnDown,Left + * 3652407,BtnUp,Left + * 3735765,SetMode,Pong + * 3735765,Screenshot,beforeFuzz.bmp + * 3802347,Fuzz, + * 10000000,Screenshot,afterFuzz.bmp + * 10000000,Quit, + * \endcode + */ + +#include "emu_ext.h" + +extern emuExtension_t replayEmuExtension; + +bool takeScreenshot(const char* name); diff --git a/emulator/src/idf/esp_log.c b/emulator/src/idf/esp_log.c index a8cac05c3..42b8caa95 100644 --- a/emulator/src/idf/esp_log.c +++ b/emulator/src/idf/esp_log.c @@ -21,6 +21,6 @@ void esp_log_write(esp_log_level_t level, const char* tag, const char* format, . vsnprintf(dbgStr, sizeof(dbgStr), format, args); va_end(args); - printf("%c| %s\n", levelChars[level], dbgStr); + printf("%c|%s| %s\n", levelChars[level], tag, dbgStr); } } diff --git a/emulator/src/idf/tinyusb.c b/emulator/src/idf/tinyusb.c new file mode 100644 index 000000000..84362c8be --- /dev/null +++ b/emulator/src/idf/tinyusb.c @@ -0,0 +1,21 @@ +#include "emu_main.h" +#include "tinyusb.h" + +esp_err_t tinyusb_driver_install(const tinyusb_config_t* config) +{ + WARN_UNIMPLEMENTED(); + return ESP_OK; +} + +bool tud_ready(void) +{ + WARN_UNIMPLEMENTED(); + return true; +} + +bool tud_hid_gamepad_report(uint8_t report_id, int8_t x, int8_t y, int8_t z, int8_t rz, int8_t rx, int8_t ry, + uint8_t hat, uint32_t buttons) +{ + WARN_UNIMPLEMENTED(); + return true; +} \ No newline at end of file diff --git a/emulator/src/rawdraw_sf.h b/emulator/src/rawdraw_sf.h index a34168786..756d4287c 100644 --- a/emulator/src/rawdraw_sf.h +++ b/emulator/src/rawdraw_sf.h @@ -7789,4 +7789,3 @@ float tdPerlin2D( float x, float y ) #endif - diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index 423bcd355..d762a1af6 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -21,13 +21,51 @@ idf_component_register(SRCS "swadge2024.c" "modes/dance/dance.c" "modes/dance/portableDance.c" "modes/demo/demoMode.c" + "modes/gamepad/gamepad.c" + "modes/lumberjack/lumberjack.c" + "modes/lumberjack/lumberjackGame.c" + "modes/lumberjack/lumberjackEntity.c" + "modes/lumberjack/lumberjackPlayer.c" "modes/jukebox/jukebox.c" "modes/mainMenu/mainMenu.c" + "modes/marbles/marbles.c" + "modes/mfpaint/mode_paint.c" + "modes/mfpaint/paint_brush.c" + "modes/mfpaint/paint_common.c" + "modes/mfpaint/paint_draw.c" + "modes/mfpaint/paint_gallery.c" + "modes/mfpaint/paint_help.c" + "modes/mfpaint/paint_nvs.c" + "modes/mfpaint/paint_share.c" + "modes/mfpaint/paint_song.c" + "modes/mfpaint/paint_ui.c" + "modes/mfpaint/paint_util.c" + "modes/mfpaint/px_stack.c" + "modes/pushy/pushy.c" "modes/pong/pong.c" + "modes/breakout/breakout.c" + "modes/breakout/tilemap.c" + "modes/breakout/entityManager.c" + "modes/breakout/entity.c" + "modes/breakout/gameData.c" + "modes/breakout/soundManager.c" + "modes/breakout/aabb_utils.c" + "modes/breakout/starfield.c" "modes/touchTest/touchTest.c" "modes/quickSettings/menuQuickSettingsRenderer.c" "modes/quickSettings/quickSettings.c" + "modes/ray/fp_math.c" + "modes/ray/mode_ray.c" + "modes/ray/ray_renderer.c" + "modes/ray/ray_map.c" + "modes/ray/ray_object.c" + "modes/ray/ray_tex_manager.c" + "modes/ray/ray_player.c" "modes/tunernome/tunernome.c" + "modes/soko/soko_game.c" + "modes/soko/soko_gamerules.c" + "modes/soko/soko_input.c" + "modes/soko/soko.c" "utils/color_utils.c" "utils/geometry.c" "utils/linked_list.c" @@ -36,6 +74,7 @@ idf_component_register(SRCS "swadge2024.c" "utils/touchUtils.c" "utils/trigonometry.c" "utils/vector2d.c" + "utils/wheel_menu.c" PRIV_REQUIRES hdw-accel hdw-battmon hdw-btn @@ -61,11 +100,20 @@ idf_component_register(SRCS "swadge2024.c" "./modes/touchTest" "./modes/colorchord" "./modes/dance" + "./modes/gamepad" "./modes/jukebox" + "./modes/pushy" "./modes/pong" + "./modes/breakout" "./modes/tunernome" "./modes/mainMenu" "./modes/demo" + "./modes/soko" + "./modes/lumberjack" + "./modes/marbles" + "./modes/mfpaint" + "./modes/ray" + "./modes/demo" "./modes/quickSettings") function(spiffs_file_preprocessor) diff --git a/main/asset_loaders/spiffs_wsg.c b/main/asset_loaders/spiffs_wsg.c index 72619a8ad..61b4850b9 100644 --- a/main/asset_loaders/spiffs_wsg.c +++ b/main/asset_loaders/spiffs_wsg.c @@ -27,7 +27,7 @@ * @return true if the WSG was loaded successfully, * false if the WSG load failed and should not be used */ -bool loadWsg(char* name, wsg_t* wsg, bool spiRam) +bool loadWsg(const char* name, wsg_t* wsg, bool spiRam) { // Read and decompress file uint32_t decompressedSize = 0; diff --git a/main/asset_loaders/spiffs_wsg.h b/main/asset_loaders/spiffs_wsg.h index 5ff8661fa..9417218f3 100644 --- a/main/asset_loaders/spiffs_wsg.h +++ b/main/asset_loaders/spiffs_wsg.h @@ -40,7 +40,7 @@ #include "wsg.h" -bool loadWsg(char* name, wsg_t* wsg, bool spiRam); +bool loadWsg(const char* name, wsg_t* wsg, bool spiRam); void freeWsg(wsg_t* wsg); #endif \ No newline at end of file diff --git a/main/colorchord/embeddedOut.h b/main/colorchord/embeddedOut.h index 6052e8c0a..2743a3394 100644 --- a/main/colorchord/embeddedOut.h +++ b/main/colorchord/embeddedOut.h @@ -16,8 +16,8 @@ #define NERF_NOTE_PORP 15 // value from 0 to 255 #endif -#ifndef USE_NUM_LIN_LEDS - #define USE_NUM_LIN_LEDS CONFIG_NUM_LEDS +#ifndef USE_CONFIG_NUM_LEDS + #define USE_CONFIG_NUM_LEDS CONFIG_NUM_LEDS #endif #ifndef LIN_WRAPAROUND diff --git a/main/idf_component.yml b/main/idf_component.yml index ab02e0669..467c94d0b 100644 --- a/main/idf_component.yml +++ b/main/idf_component.yml @@ -1,10 +1,10 @@ ## IDF Component Manager Manifest File dependencies: - espressif/tinyusb: "^0.14.3" - espressif/esp_tinyusb: "^1.3.0" + espressif/tinyusb: "^0.15.0~2" + espressif/esp_tinyusb: "^1.4.0" ## Required IDF version idf: - version: ">=5.1.0" + version: ">=5.1.1" # # Put list of dependencies here # # For components maintained by Espressif: # component: "~1.0.0" diff --git a/main/menu/menu.c b/main/menu/menu.c index 19398bae3..01989d32c 100644 --- a/main/menu/menu.c +++ b/main/menu/menu.c @@ -25,7 +25,7 @@ static void deinitSubMenu(menu_t* menu); //============================================================================== /** - * Initialize and return an empty menu. A menu is a collection of vertically + * @brief Initialize and return an empty menu. A menu is a collection of vertically * scrollable rows. The rows are separated into pages, and when a page * boundary is crossed the whole page scrolls. * @@ -42,7 +42,7 @@ static void deinitSubMenu(menu_t* menu); * the menu. * @param cbFunc The function to call when a menu option is selected. The * argument to the callback will be the same pointer - * @return A pointer to the newly allocated menu_t. This must be deinitialized + * @return A pointer to the newly allocated menu_t. This must be de-initialized * when the menu is not used anymore. */ menu_t* initMenu(const char* title, menuCb cbFunc) @@ -57,7 +57,7 @@ menu_t* initMenu(const char* title, menuCb cbFunc) } /** - * Deinitialize a menu and all connected menus, including submenus and parent + * @brief Deinitialize a menu and all connected menus, including submenus and parent * menus. This frees memory allocated for this menu, but not memory allocated * elsewhere, like the font or item labels * @@ -76,7 +76,7 @@ void deinitMenu(menu_t* menu) } /** - * Deinitialize a menu and all of its submenus recursively. This frees memory + * @brief Deinitialize a menu and all of its submenus recursively. This frees memory * allocated for this menu, but not memory allocated elsewhere, like the font or * item labels * @@ -112,7 +112,7 @@ static void deinitSubMenu(menu_t* menu) } /** - * Add a submenu item to the menu. When this item is select, the submenu is + * @brief Add a submenu item to the menu. When this item is select, the submenu is * rendered. The ::menuCb is not called. * * All items added to this ::menu_t after calling startSubMenu() will be added @@ -156,7 +156,7 @@ menu_t* startSubMenu(menu_t* menu, const char* label) } /** - * Finish adding items to a submenu and resume adding items to the parent menu. + * @brief Finish adding items to a submenu and resume adding items to the parent menu. * This will automatically add a "Back" item to the submenu, which returns to * the parent menu. ::menuCb will not be called when "Back" is selected. * @@ -180,7 +180,7 @@ menu_t* endSubMenu(menu_t* menu) } /** - * Add a single item entry to the menu. When this item is selected, the ::menuCb + * @brief Add a single item entry to the menu. When this item is selected, the ::menuCb * callback is called with the given label as the argument. * * @param menu The menu to add a single item to @@ -205,7 +205,7 @@ void addSingleItemToMenu(menu_t* menu, const char* label) } /** - * Remove a single item entry from the menu. This item is removed by pointer, + * @brief Remove a single item entry from the menu. This item is removed by pointer, * not by doing a string comparison. * * @param menu The menu to remove a single item from @@ -239,7 +239,7 @@ void removeSingleItemFromMenu(menu_t* menu, const char* label) } /** - * Add a multiple item entry to the menu. The multiple items exist in a single + * @brief Add a multiple item entry to the menu. The multiple items exist in a single * entry and are left-right scrollable. The ::menuCb callback will be called * each time the multi-item scrolls with the newly selected label as the * argument. The ::menuCb callback will also be called if the currently @@ -273,7 +273,7 @@ void addMultiItemToMenu(menu_t* menu, const char* const* labels, uint8_t numLabe } /** - * Remove a multi item entry from the menu. This item is removed by pointer, + * @brief Remove a multi item entry from the menu. This item is removed by pointer, * not by doing any string comparisons. * * @param menu The menu to remove a multi item from @@ -308,7 +308,7 @@ void removeMultiItemFromMenu(menu_t* menu, const char* const* labels) } /** - * Add a settings entry to the menu. A settings entry is left-right scrollable + * @brief Add a settings entry to the menu. A settings entry is left-right scrollable * where an integer setting is incremented or decremented. The ::menuCb callback * will be called each time the settings change with the newly selected value as * the argument. The ::menuCb callback will also be called if the currently @@ -373,8 +373,8 @@ void removeSettingsItemFromMenu(menu_t* menu, const char* label) } /** - * Adds a settings item entry to the menu with a specific list of options. The - * enry will be left-right scrollable. The ::menuCb callback will be called + * @brief Adds a settings item entry to the menu with a specific list of options. The + * entry will be left-right scrollable. The ::menuCb callback will be called * each time the setting-options item scrolls or is selected with the newly * selected label and setting value as the arguments. * @@ -471,7 +471,274 @@ void removeSettingsOptionsItemFromMenu(menu_t* menu, const char* const* optionLa } /** - * This must be called to pass button event from the Swadge mode to the menu. + * @brief Helper function to call the callback when a menu is navigated or an item is selected + * + * @param menu The menu to call a callback for + * @param item The item that was selected or scrolled to + * @param selected true if the item was selected, false if it was only navigated to + */ +static void menuCallCallbackForItem(menu_t* menu, menuItem_t* item, bool selected) +{ + menu->cbFunc( + // If the item is a non-setting item with options, pass the option label. Otherwise, the main label + (item->options && !item->settingVals) ? item->options[item->currentOpt] : item->label, + // Pass along selected + selected, + // If the item is a setting with options, pass the current option value. Otherwise, the regular + // setting + item->settingVals ? item->settingVals[item->currentOpt] : item->currentSetting); +} + +/** + * @brief Changes the selected item to the one with the given label, just as though it were scrolled to. + * + * @param menu The menu to change the selected item of + * @param label The label of the menu item or an option to select + * @return A pointer to the menu to use for future function calls. It may be a sub or parent menu. + */ +menu_t* menuNavigateToItem(menu_t* menu, const char* label) +{ + node_t* listNode = menu->items->first; + while (NULL != listNode) + { + menuItem_t* item = listNode->val; + + if (item->label) + { + if (item->label == label) + { + menu->currentItem = listNode; + menuCallCallbackForItem(menu, item, false); + return menu; + } + } + else if (item->options) + { + for (uint8_t i = 0; i < item->numOptions; i++) + { + if (item->options[i] == label) + { + menu->currentItem = listNode; + item->currentOpt = i; + + if (item->settingVals) + { + item->currentSetting = item->settingVals[i]; + } + + menuCallCallbackForItem(menu, item, false); + return menu; + } + } + } + + listNode = listNode->next; + } + + return menu; +} + +/** + * @brief Navigate to the previous item in the menu. This is equivalent to pressing the UP button + * + * @param menu The menu to navigate in + * @return A pointer to the menu to use for future function calls. It may be a sub or parent menu. + */ +menu_t* menuNavigateToPrevItem(menu_t* menu) +{ + if (NULL == menu->currentItem->prev) + { + menu->currentItem = menu->items->last; + } + else + { + menu->currentItem = menu->currentItem->prev; + } + + // Call the callback for the move + menuCallCallbackForItem(menu, menu->currentItem->val, false); + + return menu; +} + +/** + * @brief Navigate to the next item in the menu. This is equivalent to pressing the DOWN button + * + * @param menu The menu to navigate in + * @return A pointer to the menu to use for future function calls. It may be a sub or parent menu. + */ +menu_t* menuNavigateToNextItem(menu_t* menu) +{ + // Scroll down + if (NULL == menu->currentItem->next) + { + menu->currentItem = menu->items->first; + } + else + { + menu->currentItem = menu->currentItem->next; + } + + // Call the callback for the move + menuCallCallbackForItem(menu, menu->currentItem->val, false); + return menu; +} + +/** + * @brief Navigate to the previous option in a menu item. This is equivalent to pressing the LEFT button + * + * @param menu The menu to navigate in + * @return A pointer to the menu to use for future function calls. It may be a sub or parent menu. + */ +menu_t* menuNavigateToPrevOption(menu_t* menu) +{ + // Get a pointer to the item for convenience + menuItem_t* item = menu->currentItem ? menu->currentItem->val : NULL; + + // Scroll options to the left, if applicable + if (NULL == item) + { + return menu; + } + else if (item->options) + { + if (0 == item->currentOpt && !item->settingVals) + { + item->currentOpt = item->numOptions - 1; + } + else if (item->currentOpt > 0) + { + item->currentOpt--; + } + } + else if (item->minSetting != item->maxSetting) + { + item->currentSetting = MAX(item->currentSetting - 1, item->minSetting); + } + else if (menu->parentMenu) + { + // If this item has a parent menu, return to it -- no callback + return menu->parentMenu; + } + else + { + // Don't call the callback again for items without options + return menu; + } + + // Call the callback, not selected + menuCallCallbackForItem(menu, item, false); + return menu; +} + +/** + * @brief Navigate to the next option in a menu item. This is equivalent to pressing the RIGHT button + * + * @param menu The menu to navigate in + * @return A pointer to the menu to use for future function calls. It may be a sub or parent menu. + */ +menu_t* menuNavigateToNextOption(menu_t* menu) +{ + menuItem_t* item = menu->currentItem ? menu->currentItem->val : NULL; + + // Scroll options to the right, if applicable + if (NULL == item) + { + return menu; + } + else if (item->options) + { + if (item->numOptions - 1 == item->currentOpt && !item->settingVals) + { + item->currentOpt = 0; + } + else if (item->currentOpt + 1 < item->numOptions) + { + item->currentOpt++; + } + + // Call the callback, not selected + if (item->settingVals) + { + item->currentSetting = item->settingVals[item->currentOpt]; + } + } + else if (item->minSetting != item->maxSetting) + { + item->currentSetting = MIN(item->currentSetting + 1, item->maxSetting); + } + else if (item->subMenu) + { + // If this item has a submenu, enter it + return item->subMenu; + } + else + { + // Don't call the callback again for items without options + return menu; + } + + menuCallCallbackForItem(menu, item, false); + return menu; +} + +/** + * @brief Select the current item in a menu item. This is equivalent to pressing the A button. + * + * @param menu The menu to select an item in + * @return A pointer to the menu to use for future function calls. It may be a sub or parent menu. + */ +menu_t* menuSelectCurrentItem(menu_t* menu) +{ + menuItem_t* item = menu->currentItem ? menu->currentItem->val : NULL; + + // Handle A button presses + if (NULL == item) + { + return menu; + } + else if (item->subMenu) + { + // If this item has a submenu, call the callback, then enter it + menu->cbFunc(item->label, true, 0); + return item->subMenu; + } + else if (item->settingVals) + { + menu->cbFunc(item->label, true, item->settingVals[item->currentOpt]); + } + else if (item->minSetting != item->maxSetting) + { + // Call the callback, not selected + menu->cbFunc(item->label, true, item->currentSetting); + } + else if (item->label) + { + if (item->label == mnuBackStr) + { + // If this is the back string, return the parent menu + // Reset the current item when leaving a submenu + menu->currentItem = menu->items->first; + return menu->parentMenu; + } + else + { + // If this is a single item, call the callback + menu->cbFunc(item->label, true, 0); + } + } + else if (item->options) + { + // If this is a multi item, call the callback + menu->cbFunc(item->options[item->currentOpt], true, 0); + } + + // maybe return menu? + return menu; +} + +/** + * @brief This must be called to pass button event from the Swadge mode to the menu. * If a button is passed here, it should not be handled anywhere else * * @param menu The menu to process button events for @@ -482,171 +749,28 @@ menu_t* menuButton(menu_t* menu, buttonEvt_t evt) { if (evt.down) { - // Get a pointer to the item for convenience - menuItem_t* item = menu->currentItem->val; - switch (evt.button) { case PB_UP: { // Scroll up - if (NULL == menu->currentItem->prev) - { - menu->currentItem = menu->items->last; - } - else - { - menu->currentItem = menu->currentItem->prev; - } - - // Call the callback for the move - item = menu->currentItem->val; - - menu->cbFunc( - // If the item is a non-setting item with options, pass the option label. Otherwise, the main label - (item->options && !item->settingVals) ? item->options[item->currentOpt] : item->label, false, - // If the item is a setting with options, pass the current option value. Otherwise, the regular - // setting - item->settingVals ? item->settingVals[item->currentOpt] : item->currentSetting); - - break; + return menuNavigateToPrevItem(menu); } case PB_DOWN: { - // Scroll down - if (NULL == menu->currentItem->next) - { - menu->currentItem = menu->items->first; - } - else - { - menu->currentItem = menu->currentItem->next; - } - - item = menu->currentItem->val; - - menu->cbFunc( - // If the item is a non-setting item with options, pass the option label. Otherwise, the main label - (item->options && !item->settingVals) ? item->options[item->currentOpt] : item->label, false, - // If the item is a setting with options, pass the current option value. Otherwise, the regular - // setting - item->settingVals ? item->settingVals[item->currentOpt] : item->currentSetting); - - break; + return menuNavigateToNextItem(menu); } case PB_LEFT: { - // Scroll options to the left, if applicable - if (item->options) - { - if (0 == item->currentOpt && !item->settingVals) - { - item->currentOpt = item->numOptions - 1; - } - else if (item->currentOpt > 0) - { - item->currentOpt--; - } - - // Call the callback, not selected - if (item->settingVals) - { - item->currentSetting = item->settingVals[item->currentOpt]; - menu->cbFunc(item->label, false, item->currentSetting); - } - else - { - menu->cbFunc(item->options[item->currentOpt], false, 0); - } - } - else if (item->minSetting != item->maxSetting) - { - item->currentSetting = MAX(item->currentSetting - 1, item->minSetting); - // Call the callback, not selected - menu->cbFunc(item->label, false, item->currentSetting); - } - else if (menu->parentMenu) - { - // If this item has a submenu, enter it - return menu->parentMenu; - } - break; + return menuNavigateToPrevOption(menu); } case PB_RIGHT: { - // Scroll options to the right, if applicable - if (item->options) - { - if (item->numOptions - 1 == item->currentOpt && !item->settingVals) - { - item->currentOpt = 0; - } - else if (item->currentOpt + 1 < item->numOptions) - { - item->currentOpt++; - } - - // Call the callback, not selected - if (item->settingVals) - { - item->currentSetting = item->settingVals[item->currentOpt]; - menu->cbFunc(item->label, false, item->currentSetting); - } - else - { - menu->cbFunc(item->options[item->currentOpt], false, 0); - } - } - else if (item->minSetting != item->maxSetting) - { - item->currentSetting = MIN(item->currentSetting + 1, item->maxSetting); - - // Call the callback, not selected - menu->cbFunc(item->label, false, item->currentSetting); - } - else if (item->subMenu) - { - // If this item has a submenu, enter it - return item->subMenu; - } - break; + return menuNavigateToNextOption(menu); } case PB_A: { - // Handle A button presses - if (item->subMenu) - { - // If this item has a submenu, call the callback, then enter it - menu->cbFunc(item->label, true, 0); - return item->subMenu; - } - else if (item->label == mnuBackStr) - { - // If this is the back string, return the parent menu - // Reset the current item when leaving a submenu - menu->currentItem = menu->items->first; - return menu->parentMenu; - } - else if (item->settingVals) - { - menu->cbFunc(item->label, true, item->settingVals[item->currentOpt]); - } - else if (item->minSetting != item->maxSetting) - { - // Call the callback, not selected - menu->cbFunc(item->label, true, item->currentSetting); - } - else if (item->label) - { - // If this is a single item, call the callback - menu->cbFunc(item->label, true, 0); - } - else if (item->options) - { - // If this is a multi item, call the callback - menu->cbFunc(item->options[item->currentOpt], true, 0); - } - break; + return menuSelectCurrentItem(menu); } case PB_B: { diff --git a/main/menu/menu.h b/main/menu/menu.h index 45b413625..f2f62ee45 100644 --- a/main/menu/menu.h +++ b/main/menu/menu.h @@ -166,7 +166,7 @@ typedef struct uint8_t minSetting; ///< The minimum value for settings items uint8_t maxSetting; ///< The maximum value for settings items const int32_t* settingVals; ///< The setting value options for settings-options items - uint8_t currentSetting; // The current value for settings items + uint8_t currentSetting; ///< The current value for settings items } menuItem_t; /** @@ -196,8 +196,16 @@ void addSettingsItemToMenu(menu_t* menu, const char* label, const settingParam_t void removeSettingsItemFromMenu(menu_t* menu, const char* label); void addSettingsOptionsItemToMenu(menu_t* menu, const char* settingLabel, const char* const* optionLabels, const int32_t* optionValues, uint8_t numOptions, const settingParam_t* bounds, - int32_t currentOption); + int32_t currentValue); void removeSettingsOptionsItemFromMenu(menu_t* menu, const char* const* optionLabels); + +menu_t* menuNavigateToItem(menu_t* menu, const char* label); +menu_t* menuNavigateToPrevItem(menu_t* menu); +menu_t* menuNavigateToNextItem(menu_t* menu); +menu_t* menuNavigateToPrevOption(menu_t* menu); +menu_t* menuNavigateToNextOption(menu_t* menu); +menu_t* menuSelectCurrentItem(menu_t* menu); + menu_t* menuButton(menu_t* menu, buttonEvt_t evt) __attribute__((warn_unused_result)); #endif \ No newline at end of file diff --git a/main/modes/breakout/aabb_utils.c b/main/modes/breakout/aabb_utils.c new file mode 100644 index 000000000..e7ea6f8df --- /dev/null +++ b/main/modes/breakout/aabb_utils.c @@ -0,0 +1,56 @@ +//============================================================================== +// Includes +//============================================================================== + +#include "aabb_utils.h" +//#include +#include "fill.h" + +//============================================================================== +// Functions +//============================================================================== + +/** + * @brief Draw a box + * + * @param disp The display to draw the box to + * @param box The box to draw + * @param color The color of the box to draw + * @param isFilled true to draw a filled box, false to draw an outline + * @param scalingFactor The scaling factor to apply before drawing the box + */ +void drawBox(box_t box, paletteColor_t color, bool isFilled, int32_t scalingFactor) +{ + if(isFilled) + { + fillDisplayArea(box.x0 >> scalingFactor, + box.y0 >> scalingFactor, + box.x1 >> scalingFactor, + box.y1 >> scalingFactor, + color); + } + else + { + /*plotRect(box.x0 >> scalingFactor, + box.y0 >> scalingFactor, + box.x1 >> scalingFactor, + box.y1 >> scalingFactor, + color);*/ + } +} + +/** + * @brief + * + * @param box0 A box to check for collision + * @param box1 The other box to check for collision + * @param scalingFactor The factor to scale the boxes by before checking for collision + * @return true if the boxes collide, false if they do not + */ +bool boxesCollide(box_t box0, box_t box1, int32_t scalingFactor) +{ + return (box0.x0 >> scalingFactor) < (box1.x1 >> scalingFactor) && + (box0.x1 >> scalingFactor) > (box1.x0 >> scalingFactor) && + (box0.y0 >> scalingFactor) < (box1.y1 >> scalingFactor) && + (box0.y1 >> scalingFactor) > (box1.y0 >> scalingFactor); +} diff --git a/main/modes/breakout/aabb_utils.h b/main/modes/breakout/aabb_utils.h new file mode 100644 index 000000000..d9fff0ba4 --- /dev/null +++ b/main/modes/breakout/aabb_utils.h @@ -0,0 +1,19 @@ +#ifndef _AABB_UTILS_H_ +#define _AABB_UTILS_H_ + +#include +#include +#include + +typedef struct +{ + int32_t x0; + int32_t y0; + int32_t x1; + int32_t y1; +} box_t; + +void drawBox(box_t box, paletteColor_t color, bool isFilled, int32_t scalingFactor); +bool boxesCollide(box_t box0, box_t box1, int32_t scalingFactor); + +#endif \ No newline at end of file diff --git a/main/modes/breakout/breakout.c b/main/modes/breakout/breakout.c new file mode 100644 index 000000000..8427b76e3 --- /dev/null +++ b/main/modes/breakout/breakout.c @@ -0,0 +1,755 @@ +/** + * @file breakout.c + * @author J.Vega (JVeg199X) + * @brief It's Galactic Breakdown. + * @date 2023-07-01 + * + */ + +//============================================================================== +// Includes +//============================================================================== + +#include "esp_random.h" +#include "breakout.h" + +#include "gameData.h" +#include "tilemap.h" +#include "soundManager.h" +#include "entityManager.h" + +#include "leveldef.h" +#include "mainMenu.h" + +#include "fill.h" +#include "starfield.h" + +#include + +//============================================================================== +// Defines +//============================================================================== + +//============================================================================== +// Enums +//============================================================================== + +//============================================================================== +// Structs +//============================================================================== +typedef void (*gameUpdateFunction_t)(breakout_t *self, int64_t elapsedUs); +struct breakout_t +{ + menu_t* menu; ///< The menu structure + menuLogbookRenderer_t* mRenderer; ///< The menu renderer + font_t logbook; ///< The font used in the menu and game + font_t ibm_vga8; + + menuItem_t* levelSelectMenuItem; + + gameData_t gameData; + tilemap_t tilemap; + entityManager_t entityManager; + + uint16_t btnState; + uint16_t prevBtnState; + + int32_t frameTimer; + + soundManager_t soundManager; + starfield_t starfield; + + gameUpdateFunction_t update; +}; + +//============================================================================== +// Function Prototypes +//============================================================================== + +static void breakoutMainLoop(int64_t elapsedUs); +static void breakoutEnterMode(void); +static void breakoutExitMode(void); + +static void breakoutMenuCb(const char* label, bool selected, uint32_t settingVal); +static void breakoutGameLoop(breakout_t *self, int64_t elapsedUs); + +static void breakoutBackgroundDrawCallback(int16_t x, int16_t y, int16_t w, int16_t h, int16_t up, int16_t upNum); + +static void drawBreakoutHud(font_t *font, gameData_t *gameData); +static void breakoutUpdateTitleScreen(breakout_t *self, int64_t elapsedUs); +static void drawBreakoutTitleScreen(font_t *font, gameData_t *gameData); +static void breakoutChangeStateReadyScreen(breakout_t *self); +static void breakoutUpdateReadyScreen(breakout_t *self, int64_t elapsedUs); +static void breakoutDrawReadyScreen(font_t *logbook, font_t *ibm_vga8, gameData_t *gameData); +static void breakoutChangeStateGame(breakout_t *self); +static void breakoutDetectGameStateChange(breakout_t *self); +static void breakoutDetectBgmChange(breakout_t *self); +static void breakoutChangeStateDead(breakout_t *self); +static void breakoutUpdateDead(breakout_t *self, int64_t elapsedUs); +static void breakoutChangeStateGameOver(breakout_t *self); +static void breakoutUpdateGameOver(breakout_t *self, int64_t elapsedUs); +static void breakoutDrawGameOver(font_t *logbook, font_t *ibm_vga8, gameData_t *gameData); +static void breakoutChangeStateTitleScreen(breakout_t *self); +static void breakoutChangeStateLevelClear(breakout_t *self); +static void breakoutUpdateLevelClear(breakout_t *self, int64_t elapsedUs); +static void breakoutDrawLevelClear(font_t *font, gameData_t *gameData); +static void breakoutChangeStateGameClear(breakout_t *self); +static void breakoutUpdateGameClear(breakout_t *self, int64_t elapsedUs); +static void breakoutDrawGameClear(font_t *ibm_vga8, font_t *logbook, gameData_t *gameData); +static void breakoutInitializeBreakoutHighScores(breakout_t* self); +static void breakoutLoadBreakoutHighScores(breakout_t* self); +static void breakoutSaveBreakoutHighScores(breakout_t* self); +static void breakoutInitializeBreakoutUnlockables(breakout_t* self); +static void breakoutLoadBreakoutUnlockables(breakout_t* self); +static void breakoutSaveBreakoutUnlockables(breakout_t* self); + +/* +static void drawBreakoutHighScores(font_t *font, breakoutHighScores_t *highScores, gameData_t *gameData); +uint8_t getHighScoreRank(breakoutHighScores_t *highScores, uint32_t newScore); +static void insertScoreIntoHighScores(breakoutHighScores_t *highScores, uint32_t newScore, char newInitials[], uint8_t rank); +*/ + +static void breakoutChangeStateNameEntry(breakout_t *self); +static void breakoutUpdateNameEntry(breakout_t *self, int64_t elapsedUs); +static void breakoutDrawNameEntry(font_t *font, gameData_t *gameData, uint8_t currentInitial); +static void breakoutChangeStateShowHighScores(breakout_t *self); +static void breakoutUpdateShowHighScores(breakout_t *self, int64_t elapsedUs); +static void breakoutDrawShowHighScores(font_t *font, uint8_t menuState); +static void breakoutChangeStatePause(breakout_t *self); +static void breakoutUpdatePause(breakout_t *self, int64_t elapsedUs); +static void breakoutDrawPause(font_t *font); +uint16_t breakoutGetLevelIndex(uint8_t world, uint8_t level); + +//============================================================================== +// Level Definitions +//============================================================================== + +#define NUM_LEVELS 11 + +static const leveldef_t leveldef[NUM_LEVELS] = { + {.filename = "intro.bin", + .timeLimit = 180}, + {.filename = "rightside.bin", + .timeLimit = 180}, + {.filename = "upsidedown.bin", + .timeLimit = 180}, + {.filename = "leftside.bin", + .timeLimit = 180}, + {.filename = "split.bin", + .timeLimit = 180}, + {.filename = "mag01.bin", + .timeLimit = 180}, + {.filename = "mag02.bin", + .timeLimit = 180}, + {.filename = "brkLvlChar1.bin", + .timeLimit = 180}, + {.filename = "bombtest.bin", + .timeLimit = 180}, + {.filename = "ponglike.bin", + .timeLimit = 180}, + {.filename = "starlite.bin", + .timeLimit = 180}, + }; + +//============================================================================== +// Look Up Tables +//============================================================================== + +static const paletteColor_t greenColors[4] = {c555, c051, c030, c051}; + +//============================================================================== +// Strings +//============================================================================== + +/* Design Pattern! + * These strings are all declared 'const' because they do not change, so that they are placed in ROM, not RAM. + * Lengths are not explicitly given so the compiler can figure it out. + */ + +static const char breakoutName[] = "Galactic Breakdown"; + +static const char breakoutNewGame[] = "New Game"; +static const char breakoutContinue[] = "Continue - Lv"; +static const char breakoutExit[] = "Exit"; + +static const char breakoutReady[] = "Get Ready!"; +static const char breakoutGameOver[] = "Game Over!"; + +static const char breakoutLevelClear[] = "Cleared!"; +static const char breakoutPause[] = "Paused"; + +//============================================================================== +// Variables +//============================================================================== + +/// The Swadge mode for Pong +swadgeMode_t breakoutMode = { + .modeName = breakoutName, + .wifiMode = NO_WIFI, + .overrideUsb = false, + .usesAccelerometer = true, + .usesThermometer = false, + .fnEnterMode = breakoutEnterMode, + .fnExitMode = breakoutExitMode, + .fnMainLoop = breakoutMainLoop, + .fnAudioCallback = NULL, + .fnBackgroundDrawCallback = breakoutBackgroundDrawCallback, + .fnEspNowRecvCb = NULL, + .fnEspNowSendCb = NULL, + .fnAdvancedUSB = NULL, +}; + +/// All state information for the Pong mode. This whole struct is calloc()'d and free()'d so that Pong is only +/// using memory while it is being played +breakout_t* breakout = NULL; + +//============================================================================== +// Functions +//============================================================================== + +/** + * @brief Enter Pong mode, allocate required memory, and initialize required variables + * + */ +static void breakoutEnterMode(void) +{ + breakout = calloc(1, sizeof(breakout_t)); + + // Load a font + loadFont("logbook.font", &breakout->logbook, false); + loadFont("ibm_vga8.font", &breakout->ibm_vga8, false); + + // Initialize the menu + breakout->menu = initMenu(breakoutName, breakoutMenuCb); + breakout->mRenderer = initMenuLogbookRenderer(&breakout->logbook); + + initializeGameData(&(breakout->gameData)); + initializeTileMap(&(breakout->tilemap)); + initializeSoundManager(&(breakout->soundManager)); + initializeEntityManager(&(breakout->entityManager), &(breakout->tilemap), &(breakout->gameData), &(breakout->soundManager)); + initializeStarfield(&(breakout->starfield)); + + breakout->tilemap.entityManager = &(breakout->entityManager); + breakout->tilemap.executeTileSpawnAll = true; + breakout->tilemap.mapOffsetX = -4; + + loadMapFromFile(&(breakout->tilemap), leveldef[0].filename); + + addSingleItemToMenu(breakout->menu, breakoutNewGame); + + /* + Manually allocate and build "level select" menu item + because the max setting will have to change as levels are unlocked + */ + + breakout->levelSelectMenuItem = calloc(1,sizeof(menuItem_t)); + breakout->levelSelectMenuItem->label = breakoutContinue; + breakout->levelSelectMenuItem->minSetting = 1; + breakout->levelSelectMenuItem->maxSetting = NUM_LEVELS; + breakout->levelSelectMenuItem->currentSetting = 1; + breakout->levelSelectMenuItem->options = NULL; + breakout->levelSelectMenuItem->subMenu = NULL; + + push(breakout->menu->items, breakout->levelSelectMenuItem); + + addSingleItemToMenu(breakout->menu, breakoutExit); + + //Set frame rate to 60 FPS + setFrameRateUs(16666); + + // Set the mode to menu mode + breakout->update = &breakoutUpdateTitleScreen; +} + +/** + * This function is called when the mode is exited. It deinitializes variables and frees all memory. + */ +static void breakoutExitMode(void) +{ + // Deinitialize the menu. + // This will also free the "level select" menu item. + deinitMenu(breakout->menu); + deinitMenuLogbookRenderer(breakout->mRenderer); + + // Free the fonts + freeFont(&breakout->logbook); + freeFont(&breakout->ibm_vga8); + + freeTilemap(&breakout->tilemap); + freeSoundManager(&breakout->soundManager); + freeEntityManager(&breakout->entityManager); + + // Free everything else + free(breakout); +} + +/** + * @brief This callback function is called when an item is selected from the menu + * + * @param label The item that was selected from the menu + * @param selected True if the item was selected with the A button, false if this is a multi-item which scrolled to + * @param settingVal The value of the setting, if the menu item is a settings item + */ +static void breakoutMenuCb(const char* label, bool selected, uint32_t settingVal) +{ + if (selected) + { + if (label == breakoutNewGame) + { + initializeGameDataFromTitleScreen(&(breakout->gameData)); + breakout->gameData.level = 1; + loadMapFromFile(&(breakout->tilemap), leveldef[0].filename); + breakout->gameData.countdown = leveldef[0].timeLimit; + breakoutChangeStateReadyScreen(breakout); + } else if (label == breakoutContinue) + { + initializeGameDataFromTitleScreen(&(breakout->gameData)); + breakout->gameData.level = settingVal; + loadMapFromFile(&(breakout->tilemap), leveldef[breakout->gameData.level - 1].filename); + breakout->gameData.countdown = leveldef[breakout->gameData.level -1].timeLimit; + breakoutChangeStateReadyScreen(breakout); + } + else if (label == breakoutExit) + { + switchToSwadgeMode(&mainMenuMode); + } + } +} + +/** + * @brief This function is called periodically and frequently. It will either draw the menu or play the game, depending + * on which screen is currently being displayed + * + * @param elapsedUs The time that has elapsed since the last call to this function, in microseconds + */ +static void breakoutMainLoop(int64_t elapsedUs) +{ + breakout->update(breakout, elapsedUs); +} + +void breakoutChangeStateTitleScreen(breakout_t *self){ + self->gameData.frameCount = 0; + self->update=&breakoutUpdateTitleScreen; +} + +static void breakoutUpdateTitleScreen(breakout_t *self, int64_t elapsedUs){ + // Process button events + buttonEvt_t evt = {0}; + while (checkButtonQueueWrapper(&evt)) + { + // Pass button events to the menu + breakout->menu = menuButton(breakout->menu, evt); + } + + // Draw the menu + drawMenuLogbook(breakout->menu, breakout->mRenderer, elapsedUs); +} + +static void breakoutChangeStateReadyScreen(breakout_t *self){ + self->gameData.frameCount = 0; + self->update = &breakoutUpdateReadyScreen; +} + +static void breakoutUpdateReadyScreen(breakout_t *self, int64_t elapsedUs){ + self->gameData.frameCount++; + if(self->gameData.frameCount > 179){ + breakoutChangeStateGame(self); + } + + updateLedsInGame(&(self->gameData)); + breakoutDrawReadyScreen(&(self->logbook), &(self->ibm_vga8), &(self->gameData)); +} + +static void breakoutDrawReadyScreen(font_t *logbook, font_t *ibm_vga8, gameData_t *gameData){ + drawBreakoutHud(ibm_vga8, gameData); + drawText(logbook, c555, breakoutReady, (TFT_WIDTH - textWidth(logbook, breakoutReady)) >> 1, 128); +} + +static void breakoutChangeStateGame(breakout_t *self){ + self->gameData.frameCount = 0; + self->gameData.playerBombsCount = 0; + deactivateAllEntities(&(self->entityManager), false, true); + self->tilemap.executeTileSpawnAll = true; + self->update = &breakoutGameLoop; +} + +/** + * @brief This function is called periodically and frequently. It runs the actual game, including processing inputs, + * physics updates and drawing to the display. + * + * @param elapsedUs The time that has elapsed since the last call to this function, in microseconds + */ +static void breakoutGameLoop(breakout_t *self, int64_t elapsedUs) +{ + buttonEvt_t evt = {0}; + while (checkButtonQueueWrapper(&evt)) + { + // Save the button state + self->gameData.btnState = evt.state; + } + updateTouchInput(&(self->gameData)); + + updateLedsInGame(&(self->gameData)); + breakoutDetectGameStateChange(self); + updateEntities(&(self->entityManager)); + + updateStarfield(&(self->starfield)); + + // Draw the field + drawStarfield(&(self->starfield)); + drawTileMap(&(self->tilemap)); + drawEntities(&(self->entityManager)); + drawBreakoutHud(&(self->ibm_vga8), &(breakout->gameData)); + + self->gameData.frameCount++; + if(self->gameData.frameCount > 59){ + self->gameData.frameCount = 0; + + if(self->gameData.countdown > 0){ + self->gameData.countdown--; + } + + self->gameData.inGameTimer++; + } + + self->gameData.prevBtnState = self->gameData.btnState; +} + +/** + * This function is called when the display driver wishes to update a + * section of the display. + * + * @param disp The display to draw to + * @param x the x coordinate that should be updated + * @param y the x coordinate that should be updated + * @param w the width of the rectangle to be updated + * @param h the height of the rectangle to be updated + * @param up update number + * @param numUp update number denominator + */ +static void breakoutBackgroundDrawCallback(int16_t x, int16_t y, int16_t w, int16_t h, int16_t up, int16_t upNum) +{ + fillDisplayArea(x, y, x + w, y + h, c000); +} + +void breakoutDetectGameStateChange(breakout_t *self){ + if(!self->gameData.changeState){ + return; + } + + switch (self->gameData.changeState) + { + case ST_DEAD: + breakoutChangeStateDead(self); + break; + + case ST_READY_SCREEN: + breakoutChangeStateReadyScreen(self); + break; + + case ST_LEVEL_CLEAR: + breakoutChangeStateLevelClear(self); + break; + + case ST_PAUSE: + breakoutChangeStatePause(self); + break; + + default: + break; + } + + self->gameData.changeState = 0; +} + +void breakoutChangeStateDead(breakout_t *self){ + self->gameData.frameCount = 0; + self->gameData.lives--; + //self->gameData.levelDeaths++; + self->gameData.combo = 0; + //self->gameData.initialHp = 1; + + //buzzer_stop(); + //buzzer_play_bgm(&sndDie); + + self->update=&breakoutUpdateDead; +} + + +void breakoutUpdateDead(breakout_t *self, int64_t elapsedUs){ + self->gameData.frameCount++; + if(self->gameData.frameCount > 179){ + resetGameDataLeds(&(self->gameData)); + if(self->gameData.lives > 0){ + breakoutChangeStateReadyScreen(self); + } else { + breakoutChangeStateGameOver(self); + } + } + + updateLedsInGame(&(self->gameData)); + updateEntities(&(self->entityManager)); + + updateStarfield(&(self->starfield)); + drawStarfield(&(self->starfield)); + + drawTileMap(&(self->tilemap)); + drawEntities(&(self->entityManager)); + drawBreakoutHud(&(self->ibm_vga8), &(self->gameData)); + + /*if(self->gameData.countdown < 0){ + drawText(self->disp, &(self->radiostars), c555, str_time_up, (self->disp->w - textWidth(&(self->radiostars), str_time_up)) / 2, 128); + }*/ +} + +void breakoutChangeStateGameOver(breakout_t *self){ + self->gameData.frameCount = 0; + resetGameDataLeds(&(self->gameData)); + //buzzer_play_bgm(&bgmGameOver); + self->update=&breakoutUpdateGameOver; +} + +void breakoutUpdateGameOver(breakout_t *self, int64_t elapsedUs){ + self->gameData.frameCount++; + if(self->gameData.frameCount > 179){ + /*//Handle unlockables + + if(self->gameData.score >= BIG_SCORE) { + self->unlockables.bigScore = true; + } + + if(self->gameData.score >= BIGGER_SCORE) { + self->unlockables.biggerScore = true; + } + + if(!self->gameData.debugMode){ + savePlatformerUnlockables(self); + } + + changeStateNameEntry(self);*/ + deactivateAllEntities(&(self->entityManager), false, false); + breakoutChangeStateTitleScreen(self); + } + + breakoutDrawGameOver(&(self->logbook), &(self->ibm_vga8), &(self->gameData)); + //updateLedsGameOver(&(self->gameData)); +} + +void breakoutDrawGameOver(font_t *logbook, font_t *ibm_vga8, gameData_t *gameData){ + drawBreakoutHud(ibm_vga8, gameData); + drawText(logbook, c555, breakoutGameOver, (TFT_WIDTH - textWidth(logbook, breakoutGameOver)) / 2, 128); +} + +static void drawBreakoutHud(font_t *font, gameData_t *gameData){ + char scoreStr[32]; + snprintf(scoreStr, sizeof(scoreStr) - 1, "%06" PRIu32, gameData->score); + + char levelStr[15]; + snprintf(levelStr, sizeof(levelStr) - 1, "Level %d", gameData->level); + + char livesStr[8]; + snprintf(livesStr, sizeof(livesStr) - 1, "x%d", gameData->lives); + + //char timeStr[10]; + //snprintf(timeStr, sizeof(timeStr) - 1, "T:%03d", gameData->countdown); + + if(gameData->frameCount > 29) { + drawText(font, c500, "1UP", 24, 2); + } + + drawText(font, c555, livesStr, 48, 2); + //drawText(font, c555, coinStr, 160, 16); + drawText(font, c555, scoreStr, 80, 2); + drawText(font, c555, levelStr, 184, 2); + //drawText(d, font, (gameData->countdown > 30) ? c555 : redColors[(gameData->frameCount >> 3) % 4], timeStr, 220, 16); + + drawText(font, c555, "B", 271, 32); + drawText(font, c555, "O", 271, 44); + drawText(font, c555, "N", 271, 56); + drawText(font, c555, "U", 271, 68); + drawText(font, c555, "S", 271, 80); + drawRect(271,96,279,96+(gameData->countdown >> 1), c555); + + //if(gameData->comboTimer == 0){ + // return; + //} + + //snprintf(scoreStr, sizeof(scoreStr) - 1, "+%" PRIu32 " (x%d)", gameData->comboScore, gameData->combo); + snprintf(scoreStr, sizeof(scoreStr) - 1, "x%d", gameData->combo); + drawText(font, /*(gameData->comboTimer < 60) ? c030:*/ greenColors[(breakout->gameData.frameCount >> 3) % 4], scoreStr, 144, 2); +} + +void breakoutChangeStateLevelClear(breakout_t *self){ + self->gameData.frameCount = 0; + resetGameDataLeds(&(self->gameData)); + self->update=&breakoutUpdateLevelClear; +} + +void breakoutUpdateLevelClear(breakout_t *self, int64_t elapsedUs){ + self->gameData.frameCount++; + self->gameData.targetBlocksBroken = 0; + + if(self->gameData.frameCount > 60){ + if(self->gameData.countdown > 0){ + self->gameData.countdown--; + + if(self->gameData.countdown % 2){ + bzrPlayBgm(&(self->soundManager.tally), BZR_STEREO); + } + + uint16_t comboPoints = 20 * self->gameData.combo; + + self->gameData.score += comboPoints; + self->gameData.comboScore = comboPoints; + + if(self->gameData.combo > 1){ + self->gameData.combo--; + } + } else if(self->gameData.frameCount % 60 == 0) { + //Hey look, it's a frame rule! + deactivateAllEntities(&(self->entityManager), false, false); + + uint16_t levelIndex = self->gameData.level - 1; + + if(levelIndex >= NUM_LEVELS - 1){ + //Game Cleared! + + //if(!self->gameData.debugMode){ + //Determine achievements + /*self->unlockables.gameCleared = true; + + if(!self->gameData.continuesUsed){ + self->unlockables.oneCreditCleared = true; + + if(self->gameData.inGameTimer < FAST_TIME) { + self->unlockables.fastTime = true; + } + } + + if(self->gameData.score >= BIG_SCORE) { + self->unlockables.bigScore = true; + } + + if(self->gameData.score >= BIGGER_SCORE) { + self->unlockables.biggerScore = true; + } + }*/ + + breakoutChangeStateGameClear(self); + return; + } else { + //Advance to the next level + self->gameData.level++; + + //Unlock the next level + levelIndex++; + /*if(levelIndex > self->unlockables.maxLevelIndexUnlocked){ + self->unlockables.maxLevelIndexUnlocked = levelIndex; + }*/ + loadMapFromFile(&(breakout->tilemap), leveldef[levelIndex].filename); + breakout->gameData.countdown = leveldef[levelIndex].timeLimit; + breakoutChangeStateReadyScreen(self); + return; + } + + /*if(!self->gameData.debugMode){ + savePlatformerUnlockables(self); + }*/ + } + } + + //updateEntities(&(self->entityManager)); + + drawStarfield(&(self->starfield)); + drawTileMap(&(self->tilemap)); + drawEntities(&(self->entityManager)); + drawBreakoutHud(&(self->ibm_vga8), &(self->gameData)); + breakoutDrawLevelClear( &(self->logbook), &(self->gameData)); + updateLedsLevelClear(&(self->gameData)); +} + +void breakoutDrawLevelClear(font_t *font, gameData_t *gameData){ + drawText(font, c555, breakoutLevelClear, (TFT_WIDTH - textWidth(font, breakoutLevelClear)) / 2, 128); +} + +void breakoutChangeStateGameClear(breakout_t *self){ + self->gameData.frameCount = 0; + self->update=&breakoutUpdateGameClear; + resetGameDataLeds(&(self->gameData)); + //buzzer_play_bgm(&bgmSmooth); +} + +void breakoutUpdateGameClear(breakout_t *self, int64_t elapsedUs){ + self->gameData.frameCount++; + + if(self->gameData.frameCount > 450){ + if(self->gameData.lives > 0){ + if(self->gameData.frameCount % 60 == 0){ + self->gameData.lives--; + self->gameData.score += 200000; + //buzzer_play_sfx(&snd1up); + } + } else if(self->gameData.frameCount % 960 == 0) { + breakoutChangeStateGameOver(self); + } + } + + drawBreakoutHud(&(self->ibm_vga8), &(self->gameData)); + breakoutDrawGameClear(&(self->ibm_vga8), &(self->logbook), &(self->gameData)); + updateLedsGameClear(&(self->gameData)); +} + +void breakoutDrawGameClear(font_t *ibm_vga8, font_t *logbook, gameData_t *gameData){ + drawBreakoutHud(ibm_vga8, gameData); + + drawText(logbook, c555, "Thanks for playing!", 16, 48); + + if(gameData->frameCount > 300){ + drawText(logbook, c555, "See you next", 8, 112); + drawText(logbook, c555, "debug mission!", 8, 160); + } + +} + +void breakoutChangeStatePause(breakout_t *self){ + //buzzer_play_bgm(&sndPause); + self->gameData.btnState = 0; + self->update=&breakoutUpdatePause; +} + +void breakoutUpdatePause(breakout_t *self, int64_t elapsedUs){ + buttonEvt_t evt = {0}; + while (checkButtonQueueWrapper(&evt)) + { + // Save the button state + breakout->gameData.btnState = evt.state; + } + + if(( + (self->gameData.btnState & PB_START) + && + !(self->gameData.prevBtnState & PB_START) + )){ + //buzzer_play_sfx(&sndPause); + //self->gameData.changeBgm = self->gameData.currentBgm; + //self->gameData.currentBgm = BGM_NULL; + self->gameData.btnState = 0; + self->update=&breakoutGameLoop; + } + + drawStarfield(&(self->starfield)); + drawTileMap(&(self->tilemap)); + drawEntities(&(self->entityManager)); + drawBreakoutHud(&(self->ibm_vga8), &(self->gameData)); + breakoutDrawPause(&(self->logbook)); + + self->gameData.prevBtnState = self->prevBtnState; +} + +void breakoutDrawPause(font_t *font){ + drawText(font, c555, breakoutPause, (TFT_WIDTH - textWidth(font, breakoutPause)) / 2, 128); +} + +uint16_t breakoutGetLevelIndex(uint8_t world, uint8_t level){ + return (world-1) * 4 + (level-1); +} \ No newline at end of file diff --git a/main/modes/breakout/breakout.h b/main/modes/breakout/breakout.h new file mode 100644 index 000000000..abd9137b2 --- /dev/null +++ b/main/modes/breakout/breakout.h @@ -0,0 +1,8 @@ +#ifndef _BREAKOUT_MODE_H_ +#define _BREAKOUT_MODE_H_ + +#include "swadge2024.h" + +extern swadgeMode_t breakoutMode; + +#endif \ No newline at end of file diff --git a/main/modes/breakout/breakout_typedef.h b/main/modes/breakout/breakout_typedef.h new file mode 100644 index 000000000..0b415e121 --- /dev/null +++ b/main/modes/breakout/breakout_typedef.h @@ -0,0 +1,49 @@ +#ifndef BREAKOUT_TYPEDEF_INCLUDED +#define BREAKOUT_TYPEDEF_INCLUDED + +typedef struct breakout_t breakout_t; +typedef struct entityManager_t entityManager_t; +typedef struct tilemap_t tilemap_t; +typedef struct entity_t entity_t; + +typedef enum { + ST_NULL, + ST_TITLE_SCREEN, + ST_READY_SCREEN, + ST_GAME, + ST_DEAD, + ST_LEVEL_CLEAR, + ST_GAME_CLEAR, + ST_GAME_OVER, + ST_HIGH_SCORE_ENTRY, + ST_HIGH_SCORE_TABLE, + ST_PAUSE +} gameStateEnum_t; + +typedef enum { + BGM_NO_CHANGE, + BGM_MAIN, + BGM_ATHLETIC, + BGM_UNDERGROUND, + BGM_FORTRESS, + BGM_NULL +} bgmEnum_t; + +typedef enum { + SP_PADDLE_0, + SP_PADDLE_1, + SP_PADDLE_2, + SP_PADDLE_VERTICAL_0, + SP_PADDLE_VERTICAL_1, + SP_PADDLE_VERTICAL_2, + SP_BALL, + SP_BOMB_0, + SP_BOMB_1, + SP_BOMB_2, + SP_EXPLOSION_0, + SP_EXPLOSION_1, + SP_EXPLOSION_2, + SP_EXPLOSION_3 +} spriteDef_t; + +#endif \ No newline at end of file diff --git a/main/modes/breakout/entity.c b/main/modes/breakout/entity.c new file mode 100644 index 000000000..c4982dc30 --- /dev/null +++ b/main/modes/breakout/entity.c @@ -0,0 +1,1247 @@ +//============================================================================== +// Includes +//============================================================================== + +#include +#include "entity.h" +#include "entityManager.h" +#include "tilemap.h" +#include "gameData.h" +#include "hdw-bzr.h" +#include "hdw-btn.h" +#include "esp_random.h" +#include "aabb_utils.h" +#include "trigonometry.h" +#include + +//============================================================================== +// Constants +//============================================================================== +#define SUBPIXEL_RESOLUTION 4 +#define TILE_SIZE_IN_POWERS_OF_2 3 +#define TILE_SIZE 8 +#define HALF_TILE_SIZE 4 +#define DESPAWN_THRESHOLD 64 + +#define SIGNOF(x) ((x > 0) - (x < 0)) +#define TO_TILE_COORDS(x) ((x) >> TILE_SIZE_IN_POWERS_OF_2) +// #define TO_PIXEL_COORDS(x) ((x) >> SUBPIXEL_RESOLUTION) +// #define TO_SUBPIXEL_COORDS(x) ((x) << SUBPIXEL_RESOLUTION) + +//============================================================================== +// Look Up Tables +//============================================================================== + +#define BOMB_EXPLOSION_TILE_CHECK_OFFSET_LENGTH 74 +static const int16_t bombExplosionTileCheckOffsets[74] = { +// X, Y + -1, -3, + 0, -3, + 1, -3, + -2, -2, + -1, -2, + 0, -2, + 1, -2, + 2, -2, + -3, -1, + -2, -1, + -1, -1, + 0, -1, + 1, -1, + 2, -1, + 3, -1, + -3, 0, + -2, 0, + -1, 0, + 0, 0, + 1, 0, + 2, 0, + 3, 0, + -3, 1, + -2, 1, + -1, 1, + 0, 1, + 1, 1, + 2, 1, + 3, 1, + -2, 2, + -1, 2, + 0, 2, + 1, 2, + 2, 2, + -1, 3, + 0, 3, + 1, 3 +}; + +//============================================================================== +// Functions +//============================================================================== +void initializeEntity(entity_t *self, entityManager_t *entityManager, tilemap_t *tilemap, gameData_t *gameData, soundManager_t *soundManager) +{ + self->active = false; + self->tilemap = tilemap; + self->gameData = gameData; + self->soundManager = soundManager; + self->homeTileX = 0; + self->homeTileY = 0; + self->entityManager = entityManager; + self->spriteFlipHorizontal = false; + self->spriteFlipVertical = false; + self->spriteRotateAngle = 0; + self->attachedToEntity = NULL; + self->shouldAdvanceMultiplier = false; + + // Fields not explicitly initialized + // self->type = 0; + // self->updateFunction = NULL; + // self->x = 0; + // self->y = 0; + // self->xspeed = 0; + // self->yspeed = 0; + // self->xMaxSpeed = 0; + // self->yMaxSpeed = 0; + // self->xDamping = 0; + // self->yDamping = 0; + // self->gravityEnabled = false; + // self->spriteIndex = 0; + // self->animationTimer = 0; + // self->jumpPower = 0; + // self->visible = false; + // self->hp = 0; + // self->invincibilityFrames = 0; + // self->scoreValue = 0; + // self->collisionHandler = NULL; + // self->tileCollisionHandler = NULL; + // self->overlapTileHandler = NULL; +}; + +void updatePlayer(entity_t *self) +{ + if(self->gameData->isTouched) + { + int32_t xdiff; + + //TODO: tune these values some more!!! + + int32_t touchIntoLevel = (self->gameData->touchX << 2) + 128; // play with this value until center touch moves paddle to center + + // the leftmost coordinate that the originX point of the paddle sprite can occupy + // | the rightmost coordinate that the originX point of the paddle sprite can occupy + touchIntoLevel = CLAMP(touchIntoLevel,608,3872); + xdiff = self->x - touchIntoLevel; + xdiff = CLAMP(xdiff, -1024, 1024); + if (self->x != touchIntoLevel) + { + self->xspeed = -xdiff; + } else { + self->xspeed = 0; + } + } else { + self->xspeed = 0; + } + + self->x += self->xspeed; + + if(self->gameData->frameCount % 10 == 0 && self->spriteIndex < SP_PADDLE_2){ + self->spriteIndex++; + } + + /* + TODO: + Move this. Doesn't need to be repeated across every paddle. + */ + if( + ( + (self->gameData->btnState & PB_START) + && + !(self->gameData->prevBtnState & PB_START) + ) + ){ + self->gameData->changeState = ST_PAUSE; + } + +}; + +void updatePlayerVertical(entity_t *self) +{ + if(self->gameData->isTouched) + { + int32_t ydiff; + + int32_t touchIntoLevel = ((984 - self->gameData->touchY)<< 2) + 128; // play with this value until center touch moves paddle to center + + // the leftmost coordinate that the originX point of the paddle sprite can occupy + // | the rightmost coordinate that the originX point of the paddle sprite can occupy + touchIntoLevel = CLAMP(touchIntoLevel,608,3488); + ydiff = self->y - touchIntoLevel; + ydiff = CLAMP(ydiff, -1024, 1024); + if (self->y != touchIntoLevel) + { + self->yspeed = -ydiff; + } else { + self->yspeed = 0; + } + } else { + self->yspeed = 0; + } + + self->y += self->yspeed; + + if(self->gameData->frameCount % 10 == 0 && self->spriteIndex < SP_PADDLE_VERTICAL_2){ + self->spriteIndex++; + } + + /* + TODO: + Move this. Doesn't need to be repeated across every paddle. + */ + if( + ( + (self->gameData->btnState & PB_START) + && + !(self->gameData->prevBtnState & PB_START) + ) + ){ + self->gameData->changeState = ST_PAUSE; + } + +}; + +void updateBall(entity_t *self) +{ + if(self->attachedToEntity != NULL){ + //Ball is caught + switch(self->attachedToEntity->type){ + case(ENTITY_PLAYER_PADDLE_BOTTOM): + self->x = self->attachedToEntity->x; + self->y = self->attachedToEntity->y-((self->entityManager->sprites[self->spriteIndex].originY + self->entityManager->sprites[self->attachedToEntity->spriteIndex].originY) << SUBPIXEL_RESOLUTION); + + if( + self->gameData->btnState & PB_UP + && + !(self->gameData->prevBtnState & PB_UP) + ) + { + //Launch ball + setVelocity(self, 90 - CLAMP((self->attachedToEntity->xspeed)/SUBPIXEL_RESOLUTION,-60,60), 63); + self->attachedToEntity = NULL; + bzrPlaySfx(&(self->soundManager->launch), BZR_STEREO); + } + break; + case(ENTITY_PLAYER_PADDLE_TOP): + self->x = self->attachedToEntity->x; + self->y = self->attachedToEntity->y+((self->entityManager->sprites[self->spriteIndex].originY + self->entityManager->sprites[self->attachedToEntity->spriteIndex].originY) << SUBPIXEL_RESOLUTION); + + if( + self->gameData->btnState & PB_UP + && + !(self->gameData->prevBtnState & PB_UP) + ) + { + //Launch ball + setVelocity(self, 270 + CLAMP((self->attachedToEntity->xspeed)/SUBPIXEL_RESOLUTION,-60,60), 63); + self->attachedToEntity = NULL; + bzrPlaySfx(&(self->soundManager->launch), BZR_STEREO); + } + break; + case(ENTITY_PLAYER_PADDLE_LEFT): + self->x = self->attachedToEntity->x+((self->entityManager->sprites[self->spriteIndex].originX + self->entityManager->sprites[self->attachedToEntity->spriteIndex].originX) << SUBPIXEL_RESOLUTION); + self->y = self->attachedToEntity->y; + + if( + self->gameData->btnState & PB_UP + && + !(self->gameData->prevBtnState & PB_UP) + ) + { + //Launch ball + setVelocity(self, 0 - CLAMP((self->attachedToEntity->yspeed)/SUBPIXEL_RESOLUTION,-60,60), 63); + self->attachedToEntity = NULL; + bzrPlaySfx(&(self->soundManager->launch), BZR_STEREO); + } + break; + case(ENTITY_PLAYER_PADDLE_RIGHT): + self->x = self->attachedToEntity->x-((self->entityManager->sprites[self->spriteIndex].originX + self->entityManager->sprites[self->attachedToEntity->spriteIndex].originX) << SUBPIXEL_RESOLUTION); + self->y = self->attachedToEntity->y; + + if( + self->gameData->btnState & PB_UP + && + !(self->gameData->prevBtnState & PB_UP) + ) + { + //Launch ball + setVelocity(self, 180 - CLAMP(-(self->attachedToEntity->yspeed)/SUBPIXEL_RESOLUTION,-60,60), 63); + self->attachedToEntity = NULL; + bzrPlaySfx(&(self->soundManager->launch), BZR_STEREO); + } + break; + default: + break; + } + } else { + //Ball is in play + moveEntityWithTileCollisions(self); + detectEntityCollisions(self); + + + if(self->gameData->bombDetonateCooldown > 0){ + self->gameData->bombDetonateCooldown--; + } + + if( + self->gameData->btnState & PB_DOWN + && + !(self->gameData->prevBtnState & PB_DOWN) + ) + { + if(self->gameData->playerBombsCount < 3){ + //Drop bomb + entity_t* createdBomb = createEntity(self->entityManager, ENTITY_PLAYER_BOMB, self->x >> SUBPIXEL_RESOLUTION, self->y >> SUBPIXEL_RESOLUTION); + if(createdBomb != NULL){ + if(self->gameData->playerBombsCount == 0){ + self->gameData->nextBombToDetonate = self->gameData->nextBombSlot; + } + + self->gameData->playerBombs[self->gameData->nextBombSlot] = createdBomb; + self->gameData->nextBombSlot = (self->gameData->nextBombSlot + 1) % 3; + self->gameData->playerBombsCount++; + + bzrPlaySfx(&(self->soundManager->dropBomb), BZR_LEFT); + } + } + } + } + + if(self->y > 3840 || self->x > 4480) { + self->gameData->changeState = ST_DEAD; + destroyEntity(self, true); + bzrPlaySfx(&(self->soundManager->die), BZR_STEREO); + } +}; + +void updateBallAtStart(entity_t *self){ + //Find a nearby paddle and attach ball to it + for (uint8_t i = 0; i < MAX_ENTITIES; i++) + { + entity_t *checkEntity = &(self->entityManager->entities[i]); + if (checkEntity->active && checkEntity != self && (checkEntity->type == ENTITY_PLAYER_PADDLE_BOTTOM || checkEntity->type == ENTITY_PLAYER_PADDLE_TOP || checkEntity->type == ENTITY_PLAYER_PADDLE_LEFT || checkEntity->type == ENTITY_PLAYER_PADDLE_RIGHT) ) + { + uint32_t dist = abs(self->x - checkEntity->x) + abs(self->y - checkEntity->y); + + if (dist < 400) + { + self->attachedToEntity = checkEntity; + self->updateFunction = &updateBall; + self->collisionHandler = &ballCollisionHandler; + } + } + } +} + +void updateBomb(entity_t * self){ + if(self->gameData->playerBombs[self->gameData->nextBombToDetonate] != self || self->gameData->bombDetonateCooldown > 0){ + return; + } + + if(self->gameData->frameCount % 5 == 0) { + self->spriteIndex = SP_BOMB_0 + ((self->spriteIndex + 1) % 2); + } + + if( + self->gameData->btnState & PB_UP + && + !(self->gameData->prevBtnState & PB_UP) + ){ + uint8_t tx = TO_TILE_COORDS(self->x >> SUBPIXEL_RESOLUTION); + uint8_t ty = TO_TILE_COORDS(self->y >> SUBPIXEL_RESOLUTION); + uint8_t ctx, cty; + + for(uint16_t i = 0; i < BOMB_EXPLOSION_TILE_CHECK_OFFSET_LENGTH; i+=2){ + ctx = tx + bombExplosionTileCheckOffsets[i]; + cty = ty + bombExplosionTileCheckOffsets[i+1]; + uint8_t tileId = getTile(self->tilemap, ctx, cty); + + switch(tileId){ + case TILE_BLOCK_1x1_RED ... TILE_UNUSED_127: { + breakBlockTile(self->tilemap, self->gameData, tileId, ctx, cty); + scorePoints(self->gameData, 10, 0); + break; + } + case TILE_BOUNDARY_1 ... TILE_BOUNDARY_3:{ + break; + } + default: { + break; + } + } + } + + destroyEntity(self, false); + createEntity(self->entityManager, ENTITY_PLAYER_BOMB_EXPLOSION, self->x >> SUBPIXEL_RESOLUTION, self->y >> SUBPIXEL_RESOLUTION); + + self->gameData->nextBombToDetonate = (self->gameData->nextBombToDetonate + 1) % 3; + self->gameData->playerBombsCount--; + self->gameData->bombDetonateCooldown = 8; + + bzrPlaySfx(&(self->soundManager->detonate), BZR_LEFT); + } +} + +void updateExplosion(entity_t * self){ + if(self->gameData->frameCount % 5 == 0) { + self->spriteIndex++; + } + + if(self->spriteIndex > SP_EXPLOSION_3){ + destroyEntity(self, false); + } +} + +void moveEntityWithTileCollisions(entity_t *self) +{ + + uint16_t newX = self->x; + uint16_t newY = self->y; + uint8_t tx = TO_TILE_COORDS(self->x >> SUBPIXEL_RESOLUTION); + uint8_t ty = TO_TILE_COORDS(self->y >> SUBPIXEL_RESOLUTION); + // bool collision = false; + + // Are we inside a block? Push self out of block + uint8_t t = getTile(self->tilemap, tx, ty); + self->overlapTileHandler(self, t, tx, ty); + + if (isSolid(t)) + { + + if (self->xspeed == 0 && self->yspeed == 0) + { + newX += (self->spriteFlipHorizontal) ? 16 : -16; + } + else + { + if (self->yspeed != 0) + { + self->yspeed = -self->yspeed; + } + else + { + self->xspeed = -self->xspeed; + } + } + } + else + { + + if (self->yspeed != 0) + { + int16_t hcof = (((self->x >> SUBPIXEL_RESOLUTION) % TILE_SIZE) - HALF_TILE_SIZE); + + // Handle halfway though tile + uint8_t at = getTile(self->tilemap, tx + SIGNOF(hcof), ty); + + if (isSolid(at)) + { + // collision = true; + newX = ((tx + 1) * TILE_SIZE - HALF_TILE_SIZE) << SUBPIXEL_RESOLUTION; + } + + uint8_t newTy = TO_TILE_COORDS(((self->y + self->yspeed) >> SUBPIXEL_RESOLUTION) + SIGNOF(self->yspeed) * HALF_TILE_SIZE); + + if (newTy != ty) + { + uint8_t newVerticalTile = getTile(self->tilemap, tx, newTy); + + //if (newVerticalTile > TILE_UNUSED_29 && newVerticalTile < TILE_BG_GOAL_ZONE) + { + if (self->tileCollisionHandler(self, newVerticalTile, tx, newTy, 2 << (self->yspeed > 0))) + { + newY = ((newTy + ((ty < newTy) ? -1 : 1)) * TILE_SIZE + HALF_TILE_SIZE) << SUBPIXEL_RESOLUTION; + } + } + } + } + + if (self->xspeed != 0) + { + int16_t vcof = (((self->y >> SUBPIXEL_RESOLUTION) % TILE_SIZE) - HALF_TILE_SIZE); + + // Handle halfway though tile + uint8_t att = getTile(self->tilemap, tx, ty + SIGNOF(vcof)); + + if (isSolid(att)) + { + // collision = true; + newY = ((ty + 1) * TILE_SIZE - HALF_TILE_SIZE) << SUBPIXEL_RESOLUTION; + } + + // Handle outside of tile + uint8_t newTx = TO_TILE_COORDS(((self->x + self->xspeed) >> SUBPIXEL_RESOLUTION) + SIGNOF(self->xspeed) * HALF_TILE_SIZE); + + if (newTx != tx) + { + uint8_t newHorizontalTile = getTile(self->tilemap, newTx, ty); + + //if (newHorizontalTile > TILE_UNUSED_29 && newHorizontalTile < TILE_BG_GOAL_ZONE) + { + if (self->tileCollisionHandler(self, newHorizontalTile, newTx, ty, (self->xspeed > 0))) + { + newX = ((newTx + ((tx < newTx) ? -1 : 1)) * TILE_SIZE + HALF_TILE_SIZE) << SUBPIXEL_RESOLUTION; + } + } + + /*if (!self->falling) + { + uint8_t newBelowTile = getTile(self->tilemap, tx, ty + 1); + + if ((self->gravityEnabled && !isSolid(newBelowTile)) ) + { + self->fallOffTileHandler(self); + } + }*/ + } + } + } + + self->x = newX + self->xspeed; + self->y = newY + self->yspeed; +} + +void destroyEntity(entity_t *self, bool respawn) +{ + if (respawn && !(self->homeTileX == 0 && self->homeTileY == 0)) + { + self->tilemap->map[self->homeTileY * self->tilemap->mapWidth + self->homeTileX] = self->type + 128; + } + + self->entityManager->activeEntities--; + self->active = false; +} + +void detectEntityCollisions(entity_t *self) +{ + sprite_t* selfSprite = &(self->entityManager->sprites[self->spriteIndex]); + box_t* selfSpriteBox = &(selfSprite->collisionBox); + + box_t selfBox; + selfBox.x0 = (self->x >> SUBPIXEL_RESOLUTION) - selfSprite->originX + selfSpriteBox->x0; + selfBox.y0 = (self->y >> SUBPIXEL_RESOLUTION) - selfSprite->originY + selfSpriteBox->y0; + selfBox.x1 = (self->x >> SUBPIXEL_RESOLUTION) - selfSprite->originX + selfSpriteBox->x1; + selfBox.y1 = (self->y >> SUBPIXEL_RESOLUTION) - selfSprite->originY + selfSpriteBox->y1; + + entity_t *checkEntity; + sprite_t* checkEntitySprite; + box_t* checkEntitySpriteBox; + box_t checkEntityBox; + + for (uint8_t i = 0; i < MAX_ENTITIES; i++) + { + checkEntity = &(self->entityManager->entities[i]); + if (checkEntity->active && checkEntity != self) + { + checkEntitySprite = &(self->entityManager->sprites[checkEntity->spriteIndex]); + checkEntitySpriteBox = &(checkEntitySprite->collisionBox); + + checkEntityBox.x0 = (checkEntity->x >> SUBPIXEL_RESOLUTION) - checkEntitySprite->originX + checkEntitySpriteBox->x0; + checkEntityBox.y0 = (checkEntity->y >> SUBPIXEL_RESOLUTION) - checkEntitySprite->originY + checkEntitySpriteBox->y0; + checkEntityBox.x1 = (checkEntity->x >> SUBPIXEL_RESOLUTION) - checkEntitySprite->originX + checkEntitySpriteBox->x1; + checkEntityBox.y1 = (checkEntity->y >> SUBPIXEL_RESOLUTION) - checkEntitySprite->originY + checkEntitySpriteBox->y1; + + if (boxesCollide(selfBox, checkEntityBox, 0)) + { + self->collisionHandler(self, checkEntity); + } + } + } +} + +void playerCollisionHandler(entity_t *self, entity_t *other) +{ + +} + +/* +void enemyCollisionHandler(entity_t *self, entity_t *other) +{ + switch (other->type) + { + case ENTITY_TEST: + case ENTITY_DUST_BUNNY: + case ENTITY_WASP: + case ENTITY_BUSH_2: + case ENTITY_BUSH_3: + case ENTITY_DUST_BUNNY_2: + case ENTITY_DUST_BUNNY_3: + case ENTITY_WASP_2: + case ENTITY_WASP_3: + case ENTITY_POWERUP: + case ENTITY_1UP: + if((self->xspeed > 0 && self->x < other->x) || (self->xspeed < 0 && self->x > other->x)){ + self->xspeed = -self->xspeed; + self->spriteFlipHorizontal = -self->spriteFlipHorizontal; + } + break; + case ENTITY_HIT_BLOCK: + self->xspeed = other->xspeed*2; + self->yspeed = other->yspeed*2; + scorePoints(self->gameData, self->scoreValue); + //buzzer_play_sfx(&sndSquish); + killEnemy(self); + break; + case ENTITY_WAVE_BALL: + self->xspeed = other->xspeed >> 1; + self->yspeed = -abs(other->xspeed >> 1); + scorePoints(self->gameData, self->scoreValue); + //buzzer_play_sfx(&sndBreak); + killEnemy(self); + destroyEntity(other, false); + break; + default: + { + break; + } + } +} +*/ + +void dummyCollisionHandler(entity_t *self, entity_t *other) +{ + return; +} + +void ballCollisionHandler(entity_t *self, entity_t *other) +{ + switch (other->type) + { + case ENTITY_PLAYER_PADDLE_BOTTOM: + if(self->yspeed > 0){ + setVelocity(self, 90 + (other->x - self->x)/SUBPIXEL_RESOLUTION, 63); + bzrPlaySfx(&(self->soundManager->hit2), BZR_LEFT); + + if(self->shouldAdvanceMultiplier){ + scorePoints(self->gameData, 0, 2 ); + self->shouldAdvanceMultiplier = false; + other->spriteIndex = SP_PADDLE_0; + } + } + break; + case ENTITY_PLAYER_PADDLE_TOP: + if(self->yspeed < 0){ + setVelocity(self, 270 + (self->x - other->x)/SUBPIXEL_RESOLUTION, 63); + bzrPlaySfx(&(self->soundManager->hit2), BZR_LEFT); + + if(self->shouldAdvanceMultiplier){ + scorePoints(self->gameData, 0, 2 ); + self->shouldAdvanceMultiplier = false; + other->spriteIndex = SP_PADDLE_0; + } + } + break; + case ENTITY_PLAYER_PADDLE_LEFT: + if(self->xspeed < 0){ + setVelocity(self, 0 + (other->y - self->y)/SUBPIXEL_RESOLUTION, 63); + bzrPlaySfx(&(self->soundManager->hit2), BZR_LEFT); + + if(self->shouldAdvanceMultiplier){ + scorePoints(self->gameData, 0, 2 ); + self->shouldAdvanceMultiplier = false; + other->spriteIndex = SP_PADDLE_VERTICAL_0; + } + } + break; + case ENTITY_PLAYER_PADDLE_RIGHT: + if(self->xspeed > 0){ + setVelocity(self, 180 + (self->y - other->y)/SUBPIXEL_RESOLUTION, 63); + bzrPlaySfx(&(self->soundManager->hit2), BZR_LEFT); + + if(self->shouldAdvanceMultiplier){ + scorePoints(self->gameData, 0, 2 ); + self->shouldAdvanceMultiplier = false; + other->spriteIndex = SP_PADDLE_VERTICAL_0; + } + } + break; + default: + { + break; + } + } +} + +bool playerTileCollisionHandler(entity_t *self, uint8_t tileId, uint8_t tx, uint8_t ty, uint8_t direction) +{ + if (isSolid(tileId)) + { + switch (direction) + { + case 0: // PB_LEFT + self->xspeed = 0; + break; + case 1: // PB_RIGHT + self->xspeed = 0; + break; + case 2: // PB_UP + self->yspeed = 0; + break; + case 4: // PB_DOWN + self->yspeed = 0; + break; + default: // Should never hit + return false; + } + // trigger tile collision resolution + return true; + } + + return false; +} + +/* +bool enemyTileCollisionHandler(entity_t *self, uint8_t tileId, uint8_t tx, uint8_t ty, uint8_t direction) +{ + switch(tileId){ + case TILE_BOUNCE_BLOCK: { + switch (direction) + { + case 0: + //hitBlock->xspeed = -64; + if(tileId == TILE_BOUNCE_BLOCK){ + self->xspeed = 48; + } + break; + case 1: + //hitBlock->xspeed = 64; + if(tileId == TILE_BOUNCE_BLOCK){ + self->xspeed = -48; + } + break; + case 2: + //hitBlock->yspeed = -128; + if(tileId == TILE_BOUNCE_BLOCK){ + self->yspeed = 48; + } + break; + case 4: + //hitBlock->yspeed = (tileId == TILE_BRICK_BLOCK) ? 32 : 64; + if(tileId == TILE_BOUNCE_BLOCK){ + self->yspeed = -48; + } + break; + default: + break; + } + break; + } + default: { + break; + } + } + + if (isSolid(tileId)) + { + switch (direction) + { + case 0: // PB_LEFT + self->xspeed = -self->xspeed; + break; + case 1: // PB_RIGHT + self->xspeed = -self->xspeed; + break; + case 2: // PB_UP + self->yspeed = 0; + break; + case 4: // PB_DOWN + // Landed on platform + self->falling = false; + self->yspeed = 0; + break; + default: // Should never hit + return false; + } + // trigger tile collision resolution + return true; + } + + return false; +} +*/ + +bool dummyTileCollisionHandler(entity_t *self, uint8_t tileId, uint8_t tx, uint8_t ty, uint8_t direction) +{ + return false; +} + +bool ballTileCollisionHandler(entity_t *self, uint8_t tileId, uint8_t tx, uint8_t ty, uint8_t direction) +{ + switch(tileId){ + case TILE_BLOCK_1x1_RED ... TILE_UNUSED_127: { + breakBlockTile(self->tilemap, self->gameData, tileId, tx, ty); + bzrPlaySfx(&(self->soundManager->hit1), BZR_LEFT); + scorePoints(self->gameData, 10, -1); + self->shouldAdvanceMultiplier = true; + break; + } + case TILE_BOUNDARY_1 ... TILE_BOUNDARY_3:{ + bzrPlaySfx(&(self->soundManager->hit3), BZR_LEFT); + break; + } + default: { + break; + } + } + + if (isSolid(tileId)) + { + switch (direction) + { + case 0: // PB_LEFT + self->xspeed = -self->xspeed; + break; + case 1: // PB_RIGHT + self->xspeed = -self->xspeed; + break; + case 2: // PB_UP + self->yspeed = -self->yspeed; + break; + case 4: // PB_DOWN + self->yspeed = -self->yspeed; + break; + default: // Should never hit + return false; + } + // trigger tile collision resolution + return true; + } + + return false; +} + +void ballOverlapTileHandler(entity_t* self, uint8_t tileId, uint8_t tx, uint8_t ty){ + switch(tileId){ + case TILE_BLOCK_1x1_RED ... TILE_UNUSED_127: { + breakBlockTile(self->tilemap, self->gameData, tileId, tx, ty); + bzrPlaySfx(&(self->soundManager->hit1), BZR_LEFT); + scorePoints(self->gameData, 1, -1); + break; + } + case TILE_BOUNDARY_1 ... TILE_BOUNDARY_3:{ + bzrPlaySfx(&(self->soundManager->hit3), BZR_LEFT); + break; + } + default: { + break; + } + } +} + +void breakBlockTile(tilemap_t *tilemap, gameData_t *gameData, uint8_t tileId, uint8_t tx, uint8_t ty){ + switch(tileId){ + case TILE_BLOCK_1x1_RED ... TILE_BLOCK_1x1_BLACK: { + setTile(tilemap, tx, ty, TILE_EMPTY); + gameData->targetBlocksBroken++; + break; + } + case TILE_BLOCK_2x1_RED_L: + case TILE_BLOCK_2x1_ORANGE_L: + case TILE_BLOCK_2x1_YELLOW_L: + case TILE_BLOCK_2x1_GREEN_L: + case TILE_BLOCK_2x1_CYAN_L: + case TILE_BLOCK_2x1_BLUE_L: + case TILE_BLOCK_2x1_PURPLE_L: + case TILE_BLOCK_2x1_MAGENTA_L: + case TILE_BLOCK_2x1_WHITE_L: + case TILE_BLOCK_2x1_TAN_L: + case TILE_BLOCK_2x1_BROWN_L: + case TILE_BLOCK_2x1_BLACK_L: + { + setTile(tilemap, tx, ty, TILE_EMPTY); + gameData->targetBlocksBroken++; + + if(isBlock(getTile(tilemap, tx+1, ty))){ + setTile(tilemap, tx+1, ty, TILE_EMPTY); + gameData->targetBlocksBroken++; + } + + break; + } + case TILE_BLOCK_2x1_RED_R: + case TILE_BLOCK_2x1_ORANGE_R: + case TILE_BLOCK_2x1_YELLOW_R: + case TILE_BLOCK_2x1_GREEN_R: + case TILE_BLOCK_2x1_CYAN_R: + case TILE_BLOCK_2x1_BLUE_R: + case TILE_BLOCK_2x1_PURPLE_R: + case TILE_BLOCK_2x1_MAGENTA_R: + case TILE_BLOCK_2x1_WHITE_R: + case TILE_BLOCK_2x1_TAN_R: + case TILE_BLOCK_2x1_BROWN_R: + case TILE_BLOCK_2x1_BLACK_R: + { + setTile(tilemap, tx, ty, TILE_EMPTY); + gameData->targetBlocksBroken++; + + if(isBlock(getTile(tilemap, tx-1, ty))){ + setTile(tilemap, tx-1, ty, TILE_EMPTY); + gameData->targetBlocksBroken++; + } + + break; + } + case TILE_BLOCK_2x2_RED_UL: + case TILE_BLOCK_2x2_ORANGE_UL: + case TILE_BLOCK_2x2_YELLOW_UL: + case TILE_BLOCK_2x2_GREEN_UL: + case TILE_BLOCK_2x2_CYAN_UL: + case TILE_BLOCK_2x2_BLUE_UL: + case TILE_BLOCK_2x2_PURPLE_UL: + case TILE_BLOCK_2x2_MAGENTA_UL: + case TILE_BLOCK_2x2_WHITE_UL: + case TILE_BLOCK_2x2_TAN_UL: + case TILE_BLOCK_2x2_BROWN_UL: + case TILE_BLOCK_2x2_BLACK_UL: + { + setTile(tilemap, tx, ty, TILE_EMPTY); + gameData->targetBlocksBroken++; + + if(isBlock(getTile(tilemap, tx+1, ty))){ + setTile(tilemap, tx+1, ty, TILE_EMPTY); + gameData->targetBlocksBroken++; + } + + if(isBlock(getTile(tilemap, tx,ty+1))){ + setTile(tilemap, tx, ty+1, TILE_EMPTY); + gameData->targetBlocksBroken++; + } + + if(isBlock(getTile(tilemap, tx+1,ty+1))){ + setTile(tilemap, tx+1, ty+1, TILE_EMPTY); + gameData->targetBlocksBroken++; + } + + break; + } + case TILE_BLOCK_2x2_RED_UR: + case TILE_BLOCK_2x2_ORANGE_UR: + case TILE_BLOCK_2x2_YELLOW_UR: + case TILE_BLOCK_2x2_GREEN_UR: + case TILE_BLOCK_2x2_CYAN_UR: + case TILE_BLOCK_2x2_BLUE_UR: + case TILE_BLOCK_2x2_PURPLE_UR: + case TILE_BLOCK_2x2_MAGENTA_UR: + case TILE_BLOCK_2x2_WHITE_UR: + case TILE_BLOCK_2x2_TAN_UR: + case TILE_BLOCK_2x2_BROWN_UR: + case TILE_BLOCK_2x2_BLACK_UR: + { + setTile(tilemap, tx, ty, TILE_EMPTY); + gameData->targetBlocksBroken++; + + if(isBlock(getTile(tilemap, tx-1, ty))){ + setTile(tilemap, tx-1, ty, TILE_EMPTY); + gameData->targetBlocksBroken++; + } + + if(isBlock(getTile(tilemap, tx,ty+1))){ + setTile(tilemap, tx, ty+1, TILE_EMPTY); + gameData->targetBlocksBroken++; + } + + if(isBlock(getTile(tilemap, tx-1,ty+1))){ + setTile(tilemap, tx-1, ty+1, TILE_EMPTY); + gameData->targetBlocksBroken++; + } + + break; + } + case TILE_BLOCK_2x2_RED_DL: + case TILE_BLOCK_2x2_ORANGE_DL: + case TILE_BLOCK_2x2_YELLOW_DL: + case TILE_BLOCK_2x2_GREEN_DL: + case TILE_BLOCK_2x2_CYAN_DL: + case TILE_BLOCK_2x2_BLUE_DL: + case TILE_BLOCK_2x2_PURPLE_DL: + case TILE_BLOCK_2x2_MAGENTA_DL: + case TILE_BLOCK_2x2_WHITE_DL: + case TILE_BLOCK_2x2_TAN_DL: + case TILE_BLOCK_2x2_BROWN_DL: + case TILE_BLOCK_2x2_BLACK_DL: + { + setTile(tilemap, tx, ty, TILE_EMPTY); + gameData->targetBlocksBroken++; + + if(isBlock(getTile(tilemap, tx+1, ty))){ + setTile(tilemap, tx+1, ty, TILE_EMPTY); + gameData->targetBlocksBroken++; + } + + if(isBlock(getTile(tilemap, tx,ty-1))){ + setTile(tilemap, tx, ty-1, TILE_EMPTY); + gameData->targetBlocksBroken++; + } + + if(isBlock(getTile(tilemap, tx+1,ty-1))){ + setTile(tilemap, tx+1, ty-1, TILE_EMPTY); + gameData->targetBlocksBroken++; + } + + break; + } + case TILE_BLOCK_2x2_RED_DR: + case TILE_BLOCK_2x2_ORANGE_DR: + case TILE_BLOCK_2x2_YELLOW_DR: + case TILE_BLOCK_2x2_GREEN_DR: + case TILE_BLOCK_2x2_CYAN_DR: + case TILE_BLOCK_2x2_BLUE_DR: + case TILE_BLOCK_2x2_PURPLE_DR: + case TILE_BLOCK_2x2_MAGENTA_DR: + case TILE_BLOCK_2x2_WHITE_DR: + case TILE_BLOCK_2x2_TAN_DR: + case TILE_BLOCK_2x2_BROWN_DR: + case TILE_BLOCK_2x2_BLACK_DR: + { + setTile(tilemap, tx, ty, TILE_EMPTY); + gameData->targetBlocksBroken++; + + if(isBlock(getTile(tilemap, tx-1, ty))){ + setTile(tilemap, tx-1, ty, TILE_EMPTY); + gameData->targetBlocksBroken++; + } + + if(isBlock(getTile(tilemap, tx,ty-1))){ + setTile(tilemap, tx, ty-1, TILE_EMPTY); + gameData->targetBlocksBroken++; + } + + if(isBlock(getTile(tilemap, tx-1,ty-1))){ + setTile(tilemap, tx-1, ty-1, TILE_EMPTY); + gameData->targetBlocksBroken++; + } + + break; + } + + default: { + break; + } + } + + setLedBreakBlock(gameData, tileId); + + if(gameData->targetBlocksBroken >= tilemap->totalTargetBlocks){ + gameData->changeState = ST_LEVEL_CLEAR; + } +}; + +void setLedBreakBlock(gameData_t *gameData, uint8_t tileId){ + uint8_t ledIndex = esp_random() % CONFIG_NUM_LEDS; + uint16_t nr = 0; + uint16_t ng = 0; + uint16_t nb = 0; + + switch(tileId){ + case TILE_BLOCK_1x1_RED: + case TILE_BLOCK_2x1_RED_L: + case TILE_BLOCK_2x1_RED_R: + case TILE_BLOCK_2x2_RED_UL: + case TILE_BLOCK_2x2_RED_UR: + case TILE_BLOCK_2x2_RED_DL: + case TILE_BLOCK_2x2_RED_DR: { + nr = 255; + break; + } + case TILE_BLOCK_1x1_ORANGE: + case TILE_BLOCK_2x1_ORANGE_L: + case TILE_BLOCK_2x1_ORANGE_R: + case TILE_BLOCK_2x2_ORANGE_UL: + case TILE_BLOCK_2x2_ORANGE_UR: + case TILE_BLOCK_2x2_ORANGE_DL: + case TILE_BLOCK_2x2_ORANGE_DR: { + nr = 255; + ng = 127; + break; + } + case TILE_BLOCK_1x1_YELLOW: + case TILE_BLOCK_2x1_YELLOW_L: + case TILE_BLOCK_2x1_YELLOW_R: + case TILE_BLOCK_2x2_YELLOW_UL: + case TILE_BLOCK_2x2_YELLOW_UR: + case TILE_BLOCK_2x2_YELLOW_DL: + case TILE_BLOCK_2x2_YELLOW_DR: { + nr = 255; + ng = 255; + break; + } + case TILE_BLOCK_1x1_GREEN: + case TILE_BLOCK_2x1_GREEN_L: + case TILE_BLOCK_2x1_GREEN_R: + case TILE_BLOCK_2x2_GREEN_UL: + case TILE_BLOCK_2x2_GREEN_UR: + case TILE_BLOCK_2x2_GREEN_DL: + case TILE_BLOCK_2x2_GREEN_DR: { + ng = 255; + break; + } + case TILE_BLOCK_1x1_CYAN: + case TILE_BLOCK_2x1_CYAN_L: + case TILE_BLOCK_2x1_CYAN_R: + case TILE_BLOCK_2x2_CYAN_UL: + case TILE_BLOCK_2x2_CYAN_UR: + case TILE_BLOCK_2x2_CYAN_DL: + case TILE_BLOCK_2x2_CYAN_DR: { + ng = 255; + nb = 255; + break; + } + case TILE_BLOCK_1x1_BLUE: + case TILE_BLOCK_2x1_BLUE_L: + case TILE_BLOCK_2x1_BLUE_R: + case TILE_BLOCK_2x2_BLUE_UL: + case TILE_BLOCK_2x2_BLUE_UR: + case TILE_BLOCK_2x2_BLUE_DL: + case TILE_BLOCK_2x2_BLUE_DR: { + nb = 255; + break; + } + case TILE_BLOCK_1x1_PURPLE: + case TILE_BLOCK_2x1_PURPLE_L: + case TILE_BLOCK_2x1_PURPLE_R: + case TILE_BLOCK_2x2_PURPLE_UL: + case TILE_BLOCK_2x2_PURPLE_UR: + case TILE_BLOCK_2x2_PURPLE_DL: + case TILE_BLOCK_2x2_PURPLE_DR: { + nr = 255; + nb = 127; + break; + } + case TILE_BLOCK_1x1_MAGENTA: + case TILE_BLOCK_2x1_MAGENTA_L: + case TILE_BLOCK_2x1_MAGENTA_R: + case TILE_BLOCK_2x2_MAGENTA_UL: + case TILE_BLOCK_2x2_MAGENTA_UR: + case TILE_BLOCK_2x2_MAGENTA_DL: + case TILE_BLOCK_2x2_MAGENTA_DR: { + nr = 255; + nb = 255; + break; + } + case TILE_BLOCK_1x1_WHITE: + case TILE_BLOCK_2x1_WHITE_L: + case TILE_BLOCK_2x1_WHITE_R: + case TILE_BLOCK_2x2_WHITE_UL: + case TILE_BLOCK_2x2_WHITE_UR: + case TILE_BLOCK_2x2_WHITE_DL: + case TILE_BLOCK_2x2_WHITE_DR: { + nr = 255; + ng = 255; + nb = 255; + break; + } + case TILE_BLOCK_1x1_TAN: + case TILE_BLOCK_2x1_TAN_L: + case TILE_BLOCK_2x1_TAN_R: + case TILE_BLOCK_2x2_TAN_UL: + case TILE_BLOCK_2x2_TAN_UR: + case TILE_BLOCK_2x2_TAN_DL: + case TILE_BLOCK_2x2_TAN_DR: { + nr = 255; + ng = 204; + nb = 103; + break; + } + case TILE_BLOCK_1x1_BROWN: + case TILE_BLOCK_2x1_BROWN_L: + case TILE_BLOCK_2x1_BROWN_R: + case TILE_BLOCK_2x2_BROWN_UL: + case TILE_BLOCK_2x2_BROWN_UR: + case TILE_BLOCK_2x2_BROWN_DL: + case TILE_BLOCK_2x2_BROWN_DR: { + nr = 153; + ng = 102; + nb = 102; + break; + } + case TILE_BLOCK_1x1_BLACK: + case TILE_BLOCK_2x1_BLACK_L: + case TILE_BLOCK_2x1_BLACK_R: + case TILE_BLOCK_2x2_BLACK_UL: + case TILE_BLOCK_2x2_BLACK_UR: + case TILE_BLOCK_2x2_BLACK_DL: + case TILE_BLOCK_2x2_BLACK_DR: { + nr = 64; + ng = 64; + nb = 64; + break; + } + default: { + break; + } + + + } + + nr += gameData->leds[ledIndex].r; + ng += gameData->leds[ledIndex].g; + nb += gameData->leds[ledIndex].b; + + gameData->leds[ledIndex].r = CLAMP(nr, 0, 255); + gameData->leds[ledIndex].g = CLAMP(ng, 0, 255); + gameData->leds[ledIndex].b = CLAMP(nb, 0, 255); + + setLeds(gameData->leds, CONFIG_NUM_LEDS); +} + +void updateDummy(entity_t *self) +{ + // Do nothing, because that's what dummies do! +} + +void setVelocity(entity_t *self, int16_t direction, int16_t magnitude){ + while (direction < 0) + { + direction += 360; + } + while (direction > 359) + { + direction -= 360; + } + + int16_t sin = getSin1024(direction); + int16_t cos = getCos1024(direction); + + self->xspeed = (magnitude * cos) / 1024; + self->yspeed = -(magnitude * sin) / 1024; +} + +void playerOverlapTileHandler(entity_t* self, uint8_t tileId, uint8_t tx, uint8_t ty){ + /*switch(tileId){ + case TILE_COIN_1...TILE_COIN_3:{ + setTile(self->tilemap, tx, ty, TILE_EMPTY); + addCoins(self->gameData, 1); + scorePoints(self->gameData, 50); + break; + } + case TILE_LADDER:{ + if(self->gravityEnabled){ + self->gravityEnabled = false; + self->xspeed = 0; + } + break; + } + default: { + break; + } + } + + if(!self->gravityEnabled && tileId != TILE_LADDER){ + self->gravityEnabled = true; + self->falling = true; + if(self->yspeed < 0){ + self->yspeed = -32; + } + }*/ +} + +void defaultOverlapTileHandler(entity_t* self, uint8_t tileId, uint8_t tx, uint8_t ty){ + //Nothing to do. +} diff --git a/main/modes/breakout/entity.h b/main/modes/breakout/entity.h new file mode 100644 index 000000000..e2eee8906 --- /dev/null +++ b/main/modes/breakout/entity.h @@ -0,0 +1,134 @@ +#ifndef _ENTITY_H_ +#define _ENTITY_H_ + +//============================================================================== +// Includes +//============================================================================== + +#include +#include +#include "breakout_typedef.h" +#include "tilemap.h" +#include "gameData.h" +#include "soundManager.h" + +//============================================================================== +// Enums +//============================================================================== + +typedef enum { + ENTITY_PLAYER_PADDLE_BOTTOM, + ENTITY_PLAYER_PADDLE_TOP, + ENTITY_PLAYER_PADDLE_LEFT, + ENTITY_PLAYER_PADDLE_RIGHT, + ENTITY_UNUSED_4, + ENTITY_UNUSED_5, + ENTITY_UNUSED_6, + ENTITY_UNUSED_7, + ENTITY_UNUSED_8, + ENTITY_UNUSED_9, + ENTITY_UNUSED_10, + ENTITY_UNUSED_11, + ENTITY_UNUSED_12, + ENTITY_UNUSED_13, + ENTITY_UNUSED_14, + ENTITY_UNUSED_15, + ENTITY_PLAYER_BALL, + ENTITY_PLAYER_BOMB, + ENTITY_PLAYER_BOMB_EXPLOSION +} entityIndex_t; + +//============================================================================== +// Structs +//============================================================================== + +typedef void(*updateFunction_t)(struct entity_t *self); +typedef void(*collisionHandler_t)(struct entity_t *self, struct entity_t *other); +typedef bool(*tileCollisionHandler_t)(struct entity_t *self, uint8_t tileId, uint8_t tx, uint8_t ty, uint8_t direction); +typedef void(*fallOffTileHandler_t)(struct entity_t *self); +typedef void(*overlapTileHandler_t)(struct entity_t *self, uint8_t tileId, uint8_t tx, uint8_t ty); + +struct entity_t +{ + bool active; + + uint8_t type; + updateFunction_t updateFunction; + + uint16_t x; + uint16_t y; + + int16_t xspeed; + int16_t yspeed; + + uint8_t spriteIndex; + bool spriteFlipHorizontal; + bool spriteFlipVertical; + int16_t spriteRotateAngle; + + uint8_t animationTimer; + + tilemap_t * tilemap; + gameData_t * gameData; + soundManager_t * soundManager; + + uint8_t homeTileX; + uint8_t homeTileY; + + int16_t jumpPower; + + bool visible; + uint8_t hp; + int8_t invincibilityFrames; + uint16_t scoreValue; + + entity_t *attachedToEntity; + bool shouldAdvanceMultiplier; + + entityManager_t *entityManager; + + collisionHandler_t collisionHandler; + tileCollisionHandler_t tileCollisionHandler; + overlapTileHandler_t overlapTileHandler; +}; + +//============================================================================== +// Prototypes +//============================================================================== +void initializeEntity(entity_t * self, entityManager_t * entityManager, tilemap_t * tilemap, gameData_t * gameData, soundManager_t * soundManager); + +void updatePlayer(entity_t * self); +void updatePlayerVertical(entity_t * self); + +void updateBall(entity_t * self); +void updateBallAtStart(entity_t *self); +void updateBomb(entity_t * self); +void updateExplosion(entity_t * self); + +void moveEntityWithTileCollisions(entity_t * self); + +void destroyEntity(entity_t *self, bool respawn); + +void detectEntityCollisions(entity_t *self); + +void playerCollisionHandler(entity_t *self, entity_t* other); +void enemyCollisionHandler(entity_t *self, entity_t *other); +void dummyCollisionHandler(entity_t *self, entity_t *other); +void ballCollisionHandler(entity_t *self, entity_t *other); + +bool playerTileCollisionHandler(entity_t *self, uint8_t tileId, uint8_t tx, uint8_t ty, uint8_t direction); +bool enemyTileCollisionHandler(entity_t *self, uint8_t tileId, uint8_t tx, uint8_t ty, uint8_t direction); +bool dummyTileCollisionHandler(entity_t *self, uint8_t tileId, uint8_t tx, uint8_t ty, uint8_t direction); +bool ballTileCollisionHandler(entity_t *self, uint8_t tileId, uint8_t tx, uint8_t ty, uint8_t direction); + +void defaultOverlapTileHandler(entity_t* self, uint8_t tileId, uint8_t tx, uint8_t ty); +void playerOverlapTileHandler(entity_t* self, uint8_t tileId, uint8_t tx, uint8_t ty); +void ballOverlapTileHandler(entity_t* self, uint8_t tileId, uint8_t tx, uint8_t ty); + +void breakBlockTile(tilemap_t *tilemap, gameData_t *gameData, uint8_t tileId, uint8_t tx, uint8_t ty); +void setLedBreakBlock(gameData_t *gameData, uint8_t tileId); + +void updateDummy(entity_t* self); +void setVelocity(entity_t *self, int16_t direction, int16_t magnitude); + +#endif diff --git a/main/modes/breakout/entityManager.c b/main/modes/breakout/entityManager.c new file mode 100644 index 000000000..0f72b8732 --- /dev/null +++ b/main/modes/breakout/entityManager.c @@ -0,0 +1,509 @@ +//============================================================================== +// Includes +//============================================================================== + +#include +#include +#include + +#include "entityManager.h" +#include "esp_random.h" +#include "palette.h" + +#include "hdw-spiffs.h" +#include "spiffs_wsg.h" + +//============================================================================== +// Constants +//============================================================================== +#define SUBPIXEL_RESOLUTION 4 + +//============================================================================== +// Functions +//============================================================================== +void initializeEntityManager(entityManager_t * entityManager, tilemap_t * tilemap, gameData_t * gameData, soundManager_t * soundManager) +{ + loadSprites(entityManager); + entityManager->entities = calloc(MAX_ENTITIES, sizeof(entity_t)); + + for(uint8_t i=0; i < MAX_ENTITIES; i++) + { + initializeEntity(&(entityManager->entities[i]), entityManager, tilemap, gameData, soundManager); + } + + entityManager->activeEntities = 0; + entityManager->tilemap = tilemap; + + + //entityManager->viewEntity = createPlayer(entityManager, entityManager->tilemap->warps[0].x * 16, entityManager->tilemap->warps[0].y * 16); + //entityManager->playerEntity = entityManager->viewEntity; +}; + +void loadSprites(entityManager_t * entityManager) +{ + loadWsg("paddle000.wsg", &(entityManager->sprites[SP_PADDLE_0].wsg), false); + entityManager->sprites[SP_PADDLE_0].originX=14; + entityManager->sprites[SP_PADDLE_0].originY=4; + entityManager->sprites[SP_PADDLE_0].collisionBox.x0 = 0; + entityManager->sprites[SP_PADDLE_0].collisionBox.x1 = 27; + entityManager->sprites[SP_PADDLE_0].collisionBox.y0 = 0; + entityManager->sprites[SP_PADDLE_0].collisionBox.y1 = 7; + + loadWsg("paddle001.wsg", &entityManager->sprites[SP_PADDLE_1].wsg, false); + entityManager->sprites[SP_PADDLE_1].originX=14; + entityManager->sprites[SP_PADDLE_1].originY=4; + entityManager->sprites[SP_PADDLE_1].collisionBox.x0 = 0; + entityManager->sprites[SP_PADDLE_1].collisionBox.x1 = 27; + entityManager->sprites[SP_PADDLE_1].collisionBox.y0 = 0; + entityManager->sprites[SP_PADDLE_1].collisionBox.y1 = 7; + + loadWsg("paddle002.wsg", &entityManager->sprites[SP_PADDLE_2].wsg, false); + entityManager->sprites[SP_PADDLE_2].originX=14; + entityManager->sprites[SP_PADDLE_2].originY=4; + entityManager->sprites[SP_PADDLE_2].collisionBox.x0 = 0; + entityManager->sprites[SP_PADDLE_2].collisionBox.x1 = 27; + entityManager->sprites[SP_PADDLE_2].collisionBox.y0 = 0; + entityManager->sprites[SP_PADDLE_2].collisionBox.y1 = 7; + + loadWsg("paddleVertical000.wsg", &entityManager->sprites[SP_PADDLE_VERTICAL_0].wsg, false); + entityManager->sprites[SP_PADDLE_VERTICAL_0].originX=4; + entityManager->sprites[SP_PADDLE_VERTICAL_0].originY=14; + entityManager->sprites[SP_PADDLE_VERTICAL_0].collisionBox.x0 = 0; + entityManager->sprites[SP_PADDLE_VERTICAL_0].collisionBox.x1 = 7; + entityManager->sprites[SP_PADDLE_VERTICAL_0].collisionBox.y0 = 0; + entityManager->sprites[SP_PADDLE_VERTICAL_0].collisionBox.y1 = 27; + + loadWsg("paddleVertical001.wsg", &entityManager->sprites[SP_PADDLE_VERTICAL_1].wsg, false); + entityManager->sprites[SP_PADDLE_VERTICAL_1].originX=4; + entityManager->sprites[SP_PADDLE_VERTICAL_1].originY=14; + entityManager->sprites[SP_PADDLE_VERTICAL_1].collisionBox.x0 = 0; + entityManager->sprites[SP_PADDLE_VERTICAL_1].collisionBox.x1 = 7; + entityManager->sprites[SP_PADDLE_VERTICAL_1].collisionBox.y0 = 0; + entityManager->sprites[SP_PADDLE_VERTICAL_1].collisionBox.y1 = 27; + + loadWsg("paddleVertical002.wsg", &entityManager->sprites[SP_PADDLE_VERTICAL_2].wsg, false); + entityManager->sprites[SP_PADDLE_VERTICAL_2].originX=4; + entityManager->sprites[SP_PADDLE_VERTICAL_2].originY=14; + entityManager->sprites[SP_PADDLE_VERTICAL_2].collisionBox.x0 = 0; + entityManager->sprites[SP_PADDLE_VERTICAL_2].collisionBox.x1 = 7; + entityManager->sprites[SP_PADDLE_VERTICAL_2].collisionBox.y0 = 0; + entityManager->sprites[SP_PADDLE_VERTICAL_2].collisionBox.y1 = 27; + + loadWsg("ball.wsg", &entityManager->sprites[SP_BALL].wsg, false); + entityManager->sprites[SP_BALL].originX=4; + entityManager->sprites[SP_BALL].originY=4; + entityManager->sprites[SP_BALL].collisionBox.x0 = 0; + entityManager->sprites[SP_BALL].collisionBox.x1 = 7; + entityManager->sprites[SP_BALL].collisionBox.y0 = 0; + entityManager->sprites[SP_BALL].collisionBox.y1 = 7; + + loadWsg("dbmb000.wsg", &entityManager->sprites[SP_BOMB_0].wsg, false); + entityManager->sprites[SP_BOMB_0].originX=4; + entityManager->sprites[SP_BOMB_0].originY=4; + entityManager->sprites[SP_BOMB_0].collisionBox.x0 = 0; + entityManager->sprites[SP_BOMB_0].collisionBox.x1 = 7; + entityManager->sprites[SP_BOMB_0].collisionBox.y0 = 0; + entityManager->sprites[SP_BOMB_0].collisionBox.y1 = 7; + + loadWsg("dbmb001.wsg", &entityManager->sprites[SP_BOMB_1].wsg, false); + entityManager->sprites[SP_BOMB_1].originX=4; + entityManager->sprites[SP_BOMB_1].originY=4; + entityManager->sprites[SP_BOMB_1].collisionBox.x0 = 0; + entityManager->sprites[SP_BOMB_1].collisionBox.x1 = 7; + entityManager->sprites[SP_BOMB_1].collisionBox.y0 = 0; + entityManager->sprites[SP_BOMB_1].collisionBox.y1 = 7; + + loadWsg("dbmb002.wsg", &entityManager->sprites[SP_BOMB_2].wsg, false); + entityManager->sprites[SP_BOMB_2].originX=4; + entityManager->sprites[SP_BOMB_2].originY=4; + entityManager->sprites[SP_BOMB_2].collisionBox.x0 = 0; + entityManager->sprites[SP_BOMB_2].collisionBox.x1 = 7; + entityManager->sprites[SP_BOMB_2].collisionBox.y0 = 0; + entityManager->sprites[SP_BOMB_2].collisionBox.y1 = 7; + + loadWsg("boom000.wsg", &entityManager->sprites[SP_EXPLOSION_0].wsg, false); + entityManager->sprites[SP_EXPLOSION_0].originX=20; + entityManager->sprites[SP_EXPLOSION_0].originY=20; + entityManager->sprites[SP_EXPLOSION_0].collisionBox.x0 = 0; + entityManager->sprites[SP_EXPLOSION_0].collisionBox.x1 = 39; + entityManager->sprites[SP_EXPLOSION_0].collisionBox.y0 = 0; + entityManager->sprites[SP_EXPLOSION_0].collisionBox.y1 = 39; + + loadWsg("boom001.wsg", &entityManager->sprites[SP_EXPLOSION_1].wsg, false); + entityManager->sprites[SP_EXPLOSION_1].originX=20; + entityManager->sprites[SP_EXPLOSION_1].originY=20; + entityManager->sprites[SP_EXPLOSION_1].collisionBox.x0 = 0; + entityManager->sprites[SP_EXPLOSION_1].collisionBox.x1 = 39; + entityManager->sprites[SP_EXPLOSION_1].collisionBox.y0 = 0; + entityManager->sprites[SP_EXPLOSION_1].collisionBox.y1 = 39; + + loadWsg("boom002.wsg", &entityManager->sprites[SP_EXPLOSION_2].wsg, false); + entityManager->sprites[SP_EXPLOSION_2].originX=20; + entityManager->sprites[SP_EXPLOSION_2].originY=20; + entityManager->sprites[SP_EXPLOSION_2].collisionBox.x0 = 0; + entityManager->sprites[SP_EXPLOSION_2].collisionBox.x1 = 39; + entityManager->sprites[SP_EXPLOSION_2].collisionBox.y0 = 0; + entityManager->sprites[SP_EXPLOSION_2].collisionBox.y1 = 39; + + loadWsg("boom003.wsg", &entityManager->sprites[SP_EXPLOSION_3].wsg, false); + entityManager->sprites[SP_EXPLOSION_3].originX=20; + entityManager->sprites[SP_EXPLOSION_3].originY=20; + entityManager->sprites[SP_EXPLOSION_3].collisionBox.x0 = 0; + entityManager->sprites[SP_EXPLOSION_3].collisionBox.x1 = 39; + entityManager->sprites[SP_EXPLOSION_3].collisionBox.y0 = 0; + entityManager->sprites[SP_EXPLOSION_3].collisionBox.y1 = 39; +}; + +void updateEntities(entityManager_t * entityManager) +{ + for(uint8_t i=0; i < MAX_ENTITIES; i++) + { + if(entityManager->entities[i].active) + { + entityManager->entities[i].updateFunction(&(entityManager->entities[i])); + + if(&(entityManager->entities[i]) == entityManager->viewEntity){ + viewFollowEntity(entityManager->tilemap, &(entityManager->entities[i])); + } + } + } +}; + +void deactivateAllEntities(entityManager_t * entityManager, bool excludePlayer, bool respawn){ + for(uint8_t i=0; i < MAX_ENTITIES; i++) + { + entity_t* currentEntity = &(entityManager->entities[i]); + if(!currentEntity->active){ + continue; + } + + destroyEntity(currentEntity, respawn); + + if(excludePlayer && currentEntity == entityManager->playerEntity){ + currentEntity->active = true; + } + } +} + +void drawEntities(entityManager_t * entityManager) +{ + for(uint8_t i=0; i < MAX_ENTITIES; i++) + { + entity_t currentEntity = entityManager->entities[i]; + + if(currentEntity.active && currentEntity.visible) + { + drawWsg(&(entityManager->sprites[currentEntity.spriteIndex].wsg), (currentEntity.x >> SUBPIXEL_RESOLUTION) - entityManager->sprites[currentEntity.spriteIndex].originX - entityManager->tilemap->mapOffsetX, (currentEntity.y >> SUBPIXEL_RESOLUTION) - entityManager->tilemap->mapOffsetY - entityManager->sprites[currentEntity.spriteIndex].originY, currentEntity.spriteFlipHorizontal, currentEntity.spriteFlipVertical, currentEntity.spriteRotateAngle); + } + } +}; + +entity_t * findInactiveEntity(entityManager_t * entityManager) +{ + if(entityManager->activeEntities == MAX_ENTITIES) + { + return NULL; + }; + + uint8_t entityIndex = 0; + + while(entityManager->entities[entityIndex].active){ + entityIndex++; + + //Extra safeguard to make sure we don't get stuck here + if(entityIndex >= MAX_ENTITIES) + { + return NULL; + } + } + + return &(entityManager->entities[entityIndex]); +} + +void viewFollowEntity(tilemap_t * tilemap, entity_t * entity){ + int16_t moveViewByX = (entity->x) >> SUBPIXEL_RESOLUTION; + int16_t moveViewByY = (entity->y > 63616) ? 0: (entity->y) >> SUBPIXEL_RESOLUTION; + + int16_t centerOfViewX = tilemap->mapOffsetX + 140; + int16_t centerOfViewY = tilemap->mapOffsetY + 120; + + //if(centerOfViewX != moveViewByX) { + moveViewByX -= centerOfViewX; + //} + + //if(centerOfViewY != moveViewByY) { + moveViewByY -= centerOfViewY; + //} + + //if(moveViewByX && moveViewByY){ + scrollTileMap(tilemap, moveViewByX, moveViewByY); + //} +} + +entity_t* createEntity(entityManager_t *entityManager, uint8_t objectIndex, uint16_t x, uint16_t y){ + if(entityManager->activeEntities == MAX_ENTITIES){ + return NULL; + } + + entity_t *createdEntity; + + switch(objectIndex){ + case ENTITY_PLAYER_PADDLE_BOTTOM: + createdEntity = createPlayer(entityManager, x, y); + break; + case ENTITY_PLAYER_PADDLE_TOP: + createdEntity = createPlayerPaddleTop(entityManager, x, y); + break; + case ENTITY_PLAYER_PADDLE_LEFT: + createdEntity = createPlayerPaddleLeft(entityManager, x, y); + break; + case ENTITY_PLAYER_PADDLE_RIGHT: + createdEntity = createPlayerPaddleRight(entityManager, x, y); + break; + case ENTITY_PLAYER_BALL: + createdEntity = createBall(entityManager, x, y); + break; + case ENTITY_PLAYER_BOMB: + createdEntity = createBomb(entityManager, x, y); + break; + case ENTITY_PLAYER_BOMB_EXPLOSION: + createdEntity = createExplosion(entityManager, x, y); + break; + default: + createdEntity = NULL; + } + + if(createdEntity != NULL) { + entityManager->activeEntities++; + } + + return createdEntity; +} + +entity_t* createPlayer(entityManager_t * entityManager, uint16_t x, uint16_t y) +{ + entity_t * entity = findInactiveEntity(entityManager); + + if(entity == NULL) { + return NULL; + } + + entity->active = true; + entity->visible = true; + entity->x = x << SUBPIXEL_RESOLUTION; + entity->y = y << SUBPIXEL_RESOLUTION; + + entity->xspeed = 0; + entity->yspeed = 0; + entity->jumpPower = 0; + entity->spriteFlipVertical = false; + entity->spriteRotateAngle = 0; + entity->hp = 1; + entity->animationTimer = 0; + + entity->type = ENTITY_PLAYER_PADDLE_BOTTOM; + entity->spriteIndex = SP_PADDLE_0; + entity->updateFunction = &updatePlayer; + entity->collisionHandler = &playerCollisionHandler; + entity->tileCollisionHandler = &playerTileCollisionHandler; + entity->overlapTileHandler = &playerOverlapTileHandler; + return entity; +} + +entity_t* createPlayerPaddleTop(entityManager_t * entityManager, uint16_t x, uint16_t y) +{ + entity_t * entity = findInactiveEntity(entityManager); + + if(entity == NULL) { + return NULL; + } + + entity->active = true; + entity->visible = true; + entity->x = x << SUBPIXEL_RESOLUTION; + entity->y = y << SUBPIXEL_RESOLUTION; + + entity->xspeed = 0; + entity->yspeed = 0; + entity->jumpPower = 0; + entity->spriteFlipVertical = true; + entity->spriteRotateAngle = 0; + entity->hp = 1; + entity->animationTimer = 0; //Used as a cooldown for shooting square wave balls + + entity->type = ENTITY_PLAYER_PADDLE_TOP; + entity->spriteIndex = SP_PADDLE_0; + entity->updateFunction = &updatePlayer; + entity->collisionHandler = &playerCollisionHandler; + entity->tileCollisionHandler = &playerTileCollisionHandler; + entity->overlapTileHandler = &playerOverlapTileHandler; + return entity; +} + +entity_t* createPlayerPaddleLeft(entityManager_t * entityManager, uint16_t x, uint16_t y) +{ + entity_t * entity = findInactiveEntity(entityManager); + + if(entity == NULL) { + return NULL; + } + + entity->active = true; + entity->visible = true; + entity->x = x << SUBPIXEL_RESOLUTION; + entity->y = y << SUBPIXEL_RESOLUTION; + + entity->xspeed = 0; + entity->yspeed = 0; + entity->jumpPower = 0; + entity->spriteFlipHorizontal = true; + entity->spriteFlipVertical = false; + entity->spriteRotateAngle = 0; + entity->hp = 1; + entity->animationTimer = 0; //Used as a cooldown for shooting square wave balls + + entity->type = ENTITY_PLAYER_PADDLE_LEFT; + entity->spriteIndex = SP_PADDLE_VERTICAL_0; + entity->updateFunction = &updatePlayerVertical; + entity->collisionHandler = &playerCollisionHandler; + entity->tileCollisionHandler = &playerTileCollisionHandler; + entity->overlapTileHandler = &playerOverlapTileHandler; + return entity; +} + +entity_t* createPlayerPaddleRight(entityManager_t * entityManager, uint16_t x, uint16_t y) +{ + entity_t * entity = findInactiveEntity(entityManager); + + if(entity == NULL) { + return NULL; + } + + entity->active = true; + entity->visible = true; + entity->x = x << SUBPIXEL_RESOLUTION; + entity->y = y << SUBPIXEL_RESOLUTION; + + entity->xspeed = 0; + entity->yspeed = 0; + entity->jumpPower = 0; + entity->spriteFlipHorizontal = false; + entity->spriteFlipVertical = false; + entity->spriteRotateAngle = 0; + entity->hp = 1; + entity->animationTimer = 0; //Used as a cooldown for shooting square wave balls + + entity->type = ENTITY_PLAYER_PADDLE_RIGHT; + entity->spriteIndex = SP_PADDLE_VERTICAL_0; + entity->updateFunction = &updatePlayerVertical; + entity->collisionHandler = &playerCollisionHandler; + entity->tileCollisionHandler = &playerTileCollisionHandler; + entity->overlapTileHandler = &playerOverlapTileHandler; + return entity; +} + +entity_t* createBall(entityManager_t * entityManager, uint16_t x, uint16_t y) +{ + entity_t * entity = findInactiveEntity(entityManager); + + if(entity == NULL) { + return NULL; + } + + entity->active = true; + entity->visible = true; + entity->x = x << SUBPIXEL_RESOLUTION; + entity->y = y << SUBPIXEL_RESOLUTION; + + entity->xspeed = 0; + entity->yspeed = 0; + entity->spriteFlipHorizontal = false; + entity->spriteFlipVertical = false; + entity->spriteRotateAngle = 0; + entity->scoreValue = 100; + entity->shouldAdvanceMultiplier = false; + + entity->type = ENTITY_PLAYER_BALL; + entity->spriteIndex = SP_BALL; + entity->updateFunction = &updateBallAtStart; + entity->collisionHandler = &dummyCollisionHandler; + entity->tileCollisionHandler = &ballTileCollisionHandler; + entity->overlapTileHandler = &ballOverlapTileHandler; + + return entity; +} + +entity_t* createBomb(entityManager_t * entityManager, uint16_t x, uint16_t y) +{ + entity_t * entity = findInactiveEntity(entityManager); + + if(entity == NULL) { + return NULL; + } + + entity->active = true; + entity->visible = true; + entity->x = x << SUBPIXEL_RESOLUTION; + entity->y = y << SUBPIXEL_RESOLUTION; + + entity->xspeed = 0; + entity->yspeed = 0; + entity->spriteFlipHorizontal = false; + entity->spriteFlipVertical = false; + entity->spriteRotateAngle = 0; + entity->scoreValue = 100; + + entity->type = ENTITY_PLAYER_BOMB; + entity->spriteIndex = SP_BOMB_0; + entity->updateFunction = &updateBomb; + entity->collisionHandler = &dummyCollisionHandler; + entity->tileCollisionHandler = &dummyTileCollisionHandler; + entity->overlapTileHandler = &defaultOverlapTileHandler; + + //Entity cannot be respawned from the tilemap + entity->homeTileX = 0; + entity->homeTileY = 0; + + return entity; +} + +entity_t* createExplosion(entityManager_t * entityManager, uint16_t x, uint16_t y) +{ + entity_t * entity = findInactiveEntity(entityManager); + + if(entity == NULL) { + return NULL; + } + + entity->active = true; + entity->visible = true; + entity->x = x << SUBPIXEL_RESOLUTION; + entity->y = y << SUBPIXEL_RESOLUTION; + + entity->xspeed = 0; + entity->yspeed = 0; + entity->spriteFlipHorizontal = false; + entity->spriteFlipVertical = false; + entity->spriteRotateAngle = 0; + entity->scoreValue = 100; + + entity->type = ENTITY_PLAYER_BOMB_EXPLOSION; + entity->spriteIndex = SP_EXPLOSION_0; + entity->updateFunction = &updateExplosion; + entity->collisionHandler = &dummyCollisionHandler; + entity->tileCollisionHandler = &dummyTileCollisionHandler; + entity->overlapTileHandler = &defaultOverlapTileHandler; + + //Entity cannot be respawned from the tilemap + entity->homeTileX = 0; + entity->homeTileY = 0; + + return entity; +} + +void freeEntityManager(entityManager_t * self){ + free(self->entities); + for(uint8_t i=0; isprites[i].wsg)); + } +} diff --git a/main/modes/breakout/entityManager.h b/main/modes/breakout/entityManager.h new file mode 100644 index 000000000..2fb3626c1 --- /dev/null +++ b/main/modes/breakout/entityManager.h @@ -0,0 +1,63 @@ +#ifndef _ENTITYMANAGER_H_ +#define _ENTITYMANAGER_H_ + +//============================================================================== +// Includes +//============================================================================== + +#include +#include +#include "breakout_typedef.h" +#include "entity.h" +#include "tilemap.h" +#include "gameData.h" +#include "hdw-tft.h" +#include "sprite.h" +#include "soundManager.h" + +//============================================================================== +// Constants +//============================================================================== +#define MAX_ENTITIES 32 +#define SPRITESET_SIZE 14 + +//============================================================================== +// Structs +//============================================================================== + +struct entityManager_t +{ + sprite_t sprites[SPRITESET_SIZE]; + entity_t * entities; + uint8_t activeEntities; + + entity_t * viewEntity; + entity_t * playerEntity; + + tilemap_t * tilemap; +}; + +//============================================================================== +// Prototypes +//============================================================================== +void initializeEntityManager(entityManager_t * entityManager, tilemap_t * tilemap, gameData_t * gameData, soundManager_t * soundManager); +void loadSprites(entityManager_t * entityManager); +void updateEntities(entityManager_t * entityManager); +void deactivateAllEntities(entityManager_t * entityManager, bool excludePlayer, bool respawn); +void drawEntities(entityManager_t * entityManager); +entity_t * findInactiveEntity(entityManager_t * entityManager); + +void viewFollowEntity(tilemap_t * tilemap, entity_t * entity); +entity_t* createEntity(entityManager_t *entityManager, uint8_t objectIndex, uint16_t x, uint16_t y); +entity_t* createPlayer(entityManager_t * entityManager, uint16_t x, uint16_t y); +entity_t* createPlayerPaddleTop(entityManager_t * entityManager, uint16_t x, uint16_t y); +entity_t* createPlayerPaddleLeft(entityManager_t * entityManager, uint16_t x, uint16_t y); +entity_t* createPlayerPaddleRight(entityManager_t * entityManager, uint16_t x, uint16_t y); + +entity_t* createBall(entityManager_t * entityManager, uint16_t x, uint16_t y); +entity_t* createBomb(entityManager_t * entityManager, uint16_t x, uint16_t y); +entity_t* createExplosion(entityManager_t * entityManager, uint16_t x, uint16_t y); + +void freeEntityManager(entityManager_t * entityManager); + +#endif diff --git a/main/modes/breakout/gameData.c b/main/modes/breakout/gameData.c new file mode 100644 index 000000000..3dd30bdb7 --- /dev/null +++ b/main/modes/breakout/gameData.c @@ -0,0 +1,268 @@ +//============================================================================== +// Includes +//============================================================================== + +#include +#include "gameData.h" +#include "entityManager.h" +//#include "platformer_sounds.h" +#include "esp_random.h" +#include "hdw-btn.h" +#include "touchUtils.h" + +//============================================================================== +// Functions +//============================================================================== + void initializeGameData(gameData_t * gameData){ + gameData->gameState = 0; + gameData->btnState = 0; + gameData->score = 0; + gameData->lives = 3; + gameData->countdown = 000; + + gameData->level = 1; + gameData->frameCount = 0; + + gameData->combo = 0; + //gameData->comboTimer = 0; + gameData->bgColor = c335; + gameData->initials[0] = 'A'; + gameData->initials[1] = 'A'; + gameData->initials[2] = 'A'; + gameData->rank = 5; + /*gameData->extraLifeCollected = false; + gameData->checkpoint = 0; + gameData->levelDeaths = 0; + gameData->initialHp = 1;*/ + gameData->debugMode = false; + gameData->continuesUsed = false; + gameData->inGameTimer = 0; + + gameData->playerBombs[0] = NULL; + gameData->playerBombs[1] = NULL; + gameData->playerBombs[2] = NULL; + + gameData->playerBombsCount = 0; + gameData->nextBombToDetonate = 0; + gameData->nextBombSlot = 0; + gameData->bombDetonateCooldown = 0; +} + + void initializeGameDataFromTitleScreen(gameData_t * gameData){ + gameData->gameState = 0; + gameData->btnState = 0; + gameData->score = 0; + gameData->lives = 3; + gameData->countdown = 000; + gameData->frameCount = 0; + + gameData->combo = 0; + //gameData->comboTimer = 0; + gameData->bgColor = c000; + gameData->currentBgm = 0; + gameData->changeBgm = 0; + gameData->continuesUsed = (gameData->level == 1) ? false : true; + gameData->inGameTimer = 0; + gameData->targetBlocksBroken = 0; + + resetGameDataLeds(gameData); + + gameData->playerBombsCount = 0; + gameData->nextBombToDetonate = 0; + gameData->nextBombSlot = 0; + gameData->bombDetonateCooldown = 0; +} + +void updateLedsHpMeter(entityManager_t *entityManager, gameData_t *gameData){ + if(entityManager->playerEntity == NULL){ + return; + } + + uint8_t hp = entityManager->playerEntity->hp; + if(hp > 3){ + hp = 3; + } + + //HP meter led pairs: + //3 4 + //2 5 + //1 6 + for (int32_t i = 1; i < 7; i++) + { + gameData->leds[i].r = 0x80; + gameData->leds[i].g = 0x00; + gameData->leds[i].b = 0x00; + } + + for (int32_t i = 1; i < 1+hp; i++) + { + gameData->leds[i].r = 0x00; + gameData->leds[i].g = 0x80; + + gameData->leds[7-i].r = 0x00; + gameData->leds[7-i].g = 0x80; + } + + setLeds(gameData->leds, CONFIG_NUM_LEDS); +} + +void scorePoints(gameData_t * gameData, uint16_t points, int16_t incCombo){ + gameData->combo += incCombo; + if(gameData->combo < 1){ + gameData->combo = 1; + } + + uint32_t comboPoints = points * gameData->combo; + + gameData->score += comboPoints; + gameData->comboScore = comboPoints; + + //gameData->comboTimer = (gameData->levelDeaths < 3) ? 240: 1; +} + +void resetGameDataLeds(gameData_t * gameData) +{ + for(uint8_t i=0;ileds[i].r = 0; + gameData->leds[i].g = 0; + gameData->leds[i].b = 0; + } + + setLeds(gameData->leds, CONFIG_NUM_LEDS); +} + +void updateLedsShowHighScores(gameData_t * gameData){ + if(( (gameData->frameCount) % 10) == 0){ + for (int32_t i = 0; i < 8; i++) + { + + if(( (gameData->frameCount >> 4) % CONFIG_NUM_LEDS) == i) { + gameData->leds[i].r = 0xF0; + gameData->leds[i].g = 0xF0; + gameData->leds[i].b = 0x00; + } + + if(gameData->leds[i].r > 0){ + gameData->leds[i].r -= 0x05; + } + + if(gameData->leds[i].g > 0){ + gameData->leds[i].g -= 0x10; + } + + if(gameData->leds[i].b > 0){ + gameData->leds[i].b = 0x00; + } + + } + } + setLeds(gameData->leds, CONFIG_NUM_LEDS); +} + +void updateLedsGameOver(gameData_t * gameData){ + if(( (gameData->frameCount) % 10) == 0){ + for (int32_t i = 0; i < 8; i++) + { + + if(( (gameData->frameCount >> 4) % CONFIG_NUM_LEDS) == i) { + gameData->leds[i].r = 0xF0; + gameData->leds[i].g = 0x00; + gameData->leds[i].b = 0x00; + } + + gameData->leds[i].r -= 0x10; + gameData->leds[i].g = 0x00; + gameData->leds[i].b = 0x00; + } + } + setLeds(gameData->leds, CONFIG_NUM_LEDS); +} + +void updateLedsLevelClear(gameData_t * gameData){ + if(( (gameData->frameCount) % 10) == 0){ + for (int32_t i = 0; i < 8; i++) + { + + if(( (gameData->frameCount >> 4) % CONFIG_NUM_LEDS) == i) { + gameData->leds[i].g = (esp_random() % 24) * (10); + gameData->leds[i].b = (esp_random() % 24) * (10); + } + + if(gameData->leds[i].r > 0){ + gameData->leds[i].r -= 0x10; + } + + if(gameData->leds[i].g > 0){ + gameData->leds[i].g -= 0x10; + } + + if(gameData->leds[i].b > 0){ + gameData->leds[i].b -= 0x10; + } + } + } + setLeds(gameData->leds, CONFIG_NUM_LEDS); +} + +void updateLedsGameClear(gameData_t * gameData){ + if(( (gameData->frameCount) % 10) == 0){ + for (int32_t i = 0; i < 8; i++) + { + + if(( (gameData->frameCount >> 4) % CONFIG_NUM_LEDS) == i) { + gameData->leds[i].r = (esp_random() % 24) * (10); + gameData->leds[i].g = (esp_random() % 24) * (10); + gameData->leds[i].b = (esp_random() % 24) * (10); + } + + if(gameData->leds[i].r > 0){ + gameData->leds[i].r -= 0x10; + } + + if(gameData->leds[i].g > 0){ + gameData->leds[i].g -= 0x10; + } + + if(gameData->leds[i].b > 0){ + gameData->leds[i].b -= 0x10; + } + } + } + setLeds(gameData->leds, CONFIG_NUM_LEDS); +} + +void updateLedsInGame(gameData_t * gameData){ + + for (int32_t i = 0; i < 8; i++) + { + if(gameData->leds[i].r > 4){ + gameData->leds[i].r -= 0x02; + } else if(gameData->leds[i].r <= 4){ + gameData->leds[i].r = 0x00; + } + + if(gameData->leds[i].g > 4){ + gameData->leds[i].g -= 0x02; + } else if(gameData->leds[i].g <= 4){ + gameData->leds[i].g = 0x00; + } + + if(gameData->leds[i].b > 4){ + gameData->leds[i].b -= 0x02; + } else if(gameData->leds[i].b <= 4){ + gameData->leds[i].b = 0x00; + } + + } + + setLeds(gameData->leds, CONFIG_NUM_LEDS); +} + +void updateTouchInput(gameData_t * gameData){ + if(getTouchJoystick(&(gameData->touchPhi), &(gameData->touchRadius), &(gameData->touchIntensity))){ + gameData->isTouched = true; + getTouchCartesian(gameData->touchPhi, gameData->touchRadius, &(gameData->touchX), &(gameData->touchY)); + } else { + gameData->isTouched = false; + } +} \ No newline at end of file diff --git a/main/modes/breakout/gameData.h b/main/modes/breakout/gameData.h new file mode 100644 index 000000000..4e44b0a9d --- /dev/null +++ b/main/modes/breakout/gameData.h @@ -0,0 +1,86 @@ +#ifndef _GAMEDATA_H_ +#define _GAMEDATA_H_ + +//============================================================================== +// Includes +//============================================================================== + +#include +#include +#include "hdw-led.h" +#include "breakout_typedef.h" +#include "palette.h" + +//============================================================================== +// Constants +//============================================================================== + +//============================================================================== +// Structs +//============================================================================== + +typedef struct +{ + int16_t btnState; + int16_t prevBtnState; + uint8_t gameState; + uint8_t changeState; + + uint32_t score; + uint8_t lives; + + int16_t countdown; + uint16_t frameCount; + + uint16_t targetBlocksBroken; + + uint8_t level; + + int16_t combo; + //int16_t comboTimer; + uint32_t comboScore; + + entity_t* playerBombs[3]; + uint8_t playerBombsCount; + uint8_t nextBombToDetonate; + uint8_t nextBombSlot; + uint8_t bombDetonateCooldown; + + int32_t touchPhi; + int32_t touchRadius; + int32_t touchIntensity; + int32_t isTouched; + int32_t touchX; + int32_t touchY; + + led_t leds[8 /*CONFIG_NUM_LEDS*/]; + + paletteColor_t bgColor; + + char initials[3]; + uint8_t rank; + bool debugMode; + + uint8_t changeBgm; + uint8_t currentBgm; + + bool continuesUsed; + uint32_t inGameTimer; +} gameData_t; + +//============================================================================== +// Functions +//============================================================================== +void initializeGameData(gameData_t * gameData); +void initializeGameDataFromTitleScreen(gameData_t * gameData); +void updateLedsHpMeter(entityManager_t *entityManager, gameData_t *gameData); +void scorePoints(gameData_t * gameData, uint16_t points, int16_t incCombo); +void resetGameDataLeds(gameData_t * gameData); +void updateLedsShowHighScores(gameData_t * gameData); +void updateLedsLevelClear(gameData_t * gameData); +void updateLedsGameClear(gameData_t * gameData); +void updateLedsGameOver(gameData_t * gameData); +void updateLedsInGame(gameData_t * gameData); +void updateTouchInput(gameData_t * gameData); + +#endif \ No newline at end of file diff --git a/main/modes/breakout/leveldef.h b/main/modes/breakout/leveldef.h new file mode 100644 index 000000000..4dc68301d --- /dev/null +++ b/main/modes/breakout/leveldef.h @@ -0,0 +1,18 @@ +#ifndef _LEVELDEF_H_ +#define _LEVELDEF_H_ + +//============================================================================== +// Includes +//============================================================================== +#include + +//============================================================================== +// Structs +//============================================================================== +typedef struct { + char filename[16]; + uint16_t timeLimit; +} leveldef_t; + + +#endif diff --git a/main/modes/breakout/soundManager.c b/main/modes/breakout/soundManager.c new file mode 100644 index 000000000..a2563a74d --- /dev/null +++ b/main/modes/breakout/soundManager.c @@ -0,0 +1,31 @@ +//============================================================================== +// Includes +//============================================================================== + +#include +#include "soundManager.h" + +//============================================================================== +// Functions +//============================================================================== +void initializeSoundManager(soundManager_t *self){ + loadSong("sndBreak2.sng", &self->hit1, false); + loadSong("sndBreak3.sng", &self->hit2, false); + loadSong("sndBounce.sng", &self->hit3, false); + loadSong("sndWaveBall.sng", &self->launch, false); + loadSong("sndBrkDie.sng", &self->die, false); + loadSong("sndTally.sng", &self->tally, false); + loadSong("sndDropBomb.sng", &self->dropBomb, false); + loadSong("sndDetonate.sng", &self->detonate, false); +} + +void freeSoundManager(soundManager_t *self){ + freeSong(&self->hit1); + freeSong(&self->hit2); + freeSong(&self->hit3); + freeSong(&self->launch); + freeSong(&self->die); + freeSong(&self->tally); + freeSong(&self->dropBomb); + freeSong(&self->detonate); +} \ No newline at end of file diff --git a/main/modes/breakout/soundManager.h b/main/modes/breakout/soundManager.h new file mode 100644 index 000000000..db19dc943 --- /dev/null +++ b/main/modes/breakout/soundManager.h @@ -0,0 +1,39 @@ +#ifndef _SOUNDMANAGER_H_ +#define _SOUNDMANAGER_H_ + +//============================================================================== +// Includes +//============================================================================== + +#include +#include +#include + +//============================================================================== +// Constants +//============================================================================== + +//============================================================================== +// Structs +//============================================================================== + +typedef struct +{ + song_t hit1; + song_t hit2; + song_t hit3; + song_t launch; + song_t die; + song_t tally; + song_t dropBomb; + song_t detonate; + +} soundManager_t; + +//============================================================================== +// Functions +//============================================================================== +void initializeSoundManager(soundManager_t *self); +void freeSoundManager(soundManager_t *self); + +#endif \ No newline at end of file diff --git a/main/modes/breakout/sprite.h b/main/modes/breakout/sprite.h new file mode 100644 index 000000000..a0295a419 --- /dev/null +++ b/main/modes/breakout/sprite.h @@ -0,0 +1,22 @@ +#ifndef _SPRITE_H_ +#define _SPRITE_H_ + +//============================================================================== +// Includes +//============================================================================== +#include +#include "wsg.h" +#include "aabb_utils.h" + +//============================================================================== +// Structs +//============================================================================== +typedef struct { + wsg_t wsg; + int16_t originX; + int16_t originY; + box_t collisionBox; +} sprite_t; + + +#endif diff --git a/main/modes/breakout/starfield.c b/main/modes/breakout/starfield.c new file mode 100644 index 000000000..0e4c94d7d --- /dev/null +++ b/main/modes/breakout/starfield.c @@ -0,0 +1,81 @@ +//============================================================================== +// Includes +//============================================================================== +#include "starfield.h" +#include +#include "hdw-tft.h" +#include "palette.h" +#include "fill.h" + +//============================================================================== +// Functions +//============================================================================== +void initializeStarfield(starfield_t *self){ + for(uint16_t i=0; istars[i].x = randomInt(-TFT_WIDTH / 2, TFT_WIDTH / 2); + self->stars[i].y = randomInt(-TFT_HEIGHT / 2, TFT_HEIGHT / 2); + self->stars[i].z = 1 + esp_random() % 1023; + } +} + +void updateStarfield(starfield_t *self){ + for(uint16_t i = 0; i < NUM_STARS; i++) + { + self->stars[i].z -= 5; + if(self->stars[i].z <= 0) + { + self->stars[i].x = randomInt(-TFT_WIDTH / 2, TFT_WIDTH / 2); + self->stars[i].y = randomInt(-TFT_HEIGHT / 2, TFT_HEIGHT / 2); + self->stars[i].z += 1024; + } + } +} + +int randomInt(int lowerBound, int upperBound) +{ + return esp_random() % (upperBound - lowerBound + 1) + lowerBound; +} + +void drawStarfield(starfield_t *self){ + //clearDisplay(); + + /* rendering */ + for(uint16_t i = 0; i < NUM_STARS; i++) + { + /* Move and size the star */ + int temp[2]; + + temp[0] = ((1024 * self->stars[i].x) / self->stars[i].z) + TFT_WIDTH / 2; + temp[1] = ((1024 * self->stars[i].y) / self->stars[i].z) + TFT_HEIGHT / 2; + + //translate(&temp, TFT_WIDTH / 2, TFT_HEIGHT / 2); + + /* Draw the star */ + if( self->stars[i].z < 205) + { + fillDisplayArea(temp[0] - 3, temp[1] - 1, temp[0] + 3, temp[1] + 1, c555); + fillDisplayArea(temp[0] - 1, temp[1] - 3, temp[0] + 1, temp[1] + 3, c555); + fillDisplayArea(temp[0] - 2, temp[1] - 2, temp[0] + 2, temp[1] + 2, c555); + setPxTft(temp[0], temp[1], c555); + } + else if (self->stars[i].z < 410) + { + fillDisplayArea(temp[0] - 2, temp[1] - 1, temp[0] + 2, temp[1] + 1, c444); + fillDisplayArea(temp[0] - 1, temp[1] - 2, temp[0] + 1, temp[1] + 2, c444); + setPxTft(temp[0], temp[1], c555); + } + else if (self->stars[i].z < 614) + { + fillDisplayArea(temp[0] - 1, temp[1], temp[0] + 2, temp[1] + 1, c333); + fillDisplayArea(temp[0], temp[1] - 1, temp[0] + 1, temp[1] + 2, c333); + } + else if (self->stars[i].z < 819) + { + fillDisplayArea(temp[0], temp[1], temp[0] + 1, temp[1] + 1, c222); + } + else + { + setPxTft(temp[0], temp[1], c222); + } + } +} \ No newline at end of file diff --git a/main/modes/breakout/starfield.h b/main/modes/breakout/starfield.h new file mode 100644 index 000000000..6c3be7423 --- /dev/null +++ b/main/modes/breakout/starfield.h @@ -0,0 +1,38 @@ +#ifndef _STARFIELD_H_ +#define _STARFIELD_H_ + +//============================================================================== +// Includes +//============================================================================== +#include + +//============================================================================== +// Defines +//============================================================================== +#define NUM_STARS 92 + +//============================================================================== +// Structs +//============================================================================== +typedef struct { + int16_t x; + int16_t y; + int16_t z; + + uint8_t color; +} star_t; + +typedef struct +{ + star_t stars[NUM_STARS] +} starfield_t; + +//============================================================================== +// Function Prototypes +//============================================================================== +void initializeStarfield(starfield_t *self); +void updateStarfield(starfield_t *self); +int randomInt(int lowerBound, int upperBound); +void drawStarfield(starfield_t *self); + +#endif diff --git a/main/modes/breakout/tilemap.c b/main/modes/breakout/tilemap.c new file mode 100644 index 000000000..176cb9144 --- /dev/null +++ b/main/modes/breakout/tilemap.c @@ -0,0 +1,446 @@ +//============================================================================== +// Includes +//============================================================================== + +#include +#include +#include +#include + +#include "spiffs_wsg.h" +#include "tilemap.h" +#include "leveldef.h" +#include "esp_random.h" + +#include "hdw-spiffs.h" + +//============================================================================== +// Function Prototypes +//============================================================================== + +// bool isInteractive(uint8_t tileId); + +//============================================================================== +// Functions +//============================================================================== + +void initializeTileMap(tilemap_t *tilemap) +{ + tilemap->mapOffsetX = 0; + tilemap->mapOffsetY = 0; + + tilemap->tileSpawnEnabled = true; + tilemap->executeTileSpawnColumn = -1; + tilemap->executeTileSpawnRow = -1; + + tilemap->animationFrame = 0; + tilemap->animationTimer = 23; + + loadTiles(tilemap); +} + +void drawTileMap(tilemap_t *tilemap) +{ + tilemap->animationTimer--; + if (tilemap->animationTimer < 0) + { + tilemap->animationFrame = ((tilemap->animationFrame + 1) % 3); + tilemap->animationTimer = 23; + } + + for (uint16_t y = (tilemap->mapOffsetY >> TILE_SIZE_IN_POWERS_OF_2); y < (tilemap->mapOffsetY >> TILE_SIZE_IN_POWERS_OF_2) + TILEMAP_DISPLAY_HEIGHT_TILES; y++) + { + if (y >= tilemap->mapHeight) + { + break; + } + + for (int32_t x = (tilemap->mapOffsetX >> TILE_SIZE_IN_POWERS_OF_2); x < (tilemap->mapOffsetX >> TILE_SIZE_IN_POWERS_OF_2) + TILEMAP_DISPLAY_WIDTH_TILES; x++) + { + if (x >= tilemap->mapWidth) + { + break; + } + else if (x < 0) + { + continue; + } + + uint8_t tile = tilemap->map[(y * tilemap->mapWidth) + x]; + + if(tile < TILE_BOUNDARY_1){ + continue; + } + + // Test animated tiles + /*if (tile == 64 || tile == 67) + { + tile += tilemap->animationFrame; + }*/ + + // Draw only non-garbage tiles + if (tile > 0 && tile < 128) + { + if(needsTransparency(tile)){ + //drawWsgSimpleFast(&tilemap->tiles[tile - 1], x * TILE_SIZE - tilemap->mapOffsetX, y * TILE_SIZE - tilemap->mapOffsetY); + drawWsgSimple(&tilemap->tiles[tile - 1], x * TILE_SIZE - tilemap->mapOffsetX, y * TILE_SIZE - tilemap->mapOffsetY); + } + else { + drawWsgTile(&tilemap->tiles[tile - 1], x * TILE_SIZE - tilemap->mapOffsetX, y * TILE_SIZE - tilemap->mapOffsetY); + } + } + else if (tile > 127 && tilemap->tileSpawnEnabled && (tilemap->executeTileSpawnColumn == x || tilemap->executeTileSpawnRow == y || tilemap->executeTileSpawnAll)) + { + tileSpawnEntity(tilemap, tile - 128, x, y); + } + } + } + + tilemap->executeTileSpawnAll = 0; +} + +void scrollTileMap(tilemap_t *tilemap, int16_t x, int16_t y) +{ + if (x != 0) + { + uint8_t oldTx = tilemap->mapOffsetX >> TILE_SIZE_IN_POWERS_OF_2; + tilemap->mapOffsetX = CLAMP(tilemap->mapOffsetX + x, tilemap->minMapOffsetX, tilemap->maxMapOffsetX); + uint8_t newTx = tilemap->mapOffsetX >> TILE_SIZE_IN_POWERS_OF_2; + + if (newTx > oldTx) + { + tilemap->executeTileSpawnColumn = oldTx + TILEMAP_DISPLAY_WIDTH_TILES; + } + else if (newTx < oldTx) + { + tilemap->executeTileSpawnColumn = newTx; + } + else + { + tilemap->executeTileSpawnColumn = -1; + } + } + + if (y != 0) + { + uint8_t oldTy = tilemap->mapOffsetY >> TILE_SIZE_IN_POWERS_OF_2; + tilemap->mapOffsetY = CLAMP(tilemap->mapOffsetY + y, tilemap->minMapOffsetY, tilemap->maxMapOffsetY); + uint8_t newTy = tilemap->mapOffsetY >> TILE_SIZE_IN_POWERS_OF_2; + + if (newTy > oldTy) + { + tilemap->executeTileSpawnRow = oldTy + TILEMAP_DISPLAY_HEIGHT_TILES; + } + else if (newTy < oldTy) + { + tilemap->executeTileSpawnRow = newTy; + } + else + { + tilemap->executeTileSpawnRow = -1; + } + } +} + +bool loadMapFromFile(tilemap_t *tilemap, const char *name) +{ + if (tilemap->map != NULL) + { + free(tilemap->map); + } + + size_t sz; + uint8_t *buf = spiffsReadFile(name, &sz, false); + + if (NULL == buf) + { + ESP_LOGE("MAP", "Failed to read %s", name); + return false; + } + + uint8_t width = buf[0]; + uint8_t height = buf[1]; + + tilemap->map = (uint8_t *)heap_caps_calloc(width * height, sizeof(uint8_t), MALLOC_CAP_SPIRAM); + memcpy(tilemap->map, &buf[2], width * height); + + tilemap->mapWidth = width; + tilemap->mapHeight = height; + + tilemap->minMapOffsetX = 0; + tilemap->maxMapOffsetX = width * TILE_SIZE - TILEMAP_DISPLAY_WIDTH_PIXELS; + + tilemap->minMapOffsetY = 0; + tilemap->maxMapOffsetY = height * TILE_SIZE - TILEMAP_DISPLAY_HEIGHT_PIXELS; + + memcpy(&(tilemap->totalTargetBlocks), &buf[2 + width * height], 2); + + free(buf); + + return true; +} + +bool loadTiles(tilemap_t *tilemap) +{ + // tiles 0 is invisible + // remember to subtract 1 from tile index before drawing tile + loadWsg("brkTile001.wsg", &tilemap->tiles[0], false); + loadWsg("brkTile002.wsg", &tilemap->tiles[1], false); + loadWsg("brkTile003.wsg", &tilemap->tiles[2], false); + tilemap->tiles[3] = tilemap->tiles[0]; + tilemap->tiles[4] = tilemap->tiles[0]; + tilemap->tiles[5] = tilemap->tiles[0]; + tilemap->tiles[6] = tilemap->tiles[0]; + tilemap->tiles[7] = tilemap->tiles[0]; + tilemap->tiles[8] = tilemap->tiles[0]; + tilemap->tiles[9] = tilemap->tiles[0]; + tilemap->tiles[10] = tilemap->tiles[0]; + tilemap->tiles[11] = tilemap->tiles[0]; + tilemap->tiles[12] = tilemap->tiles[0]; + tilemap->tiles[13] = tilemap->tiles[0]; + tilemap->tiles[14] = tilemap->tiles[0]; + loadWsg("brkTile016.wsg", &tilemap->tiles[15], false); + loadWsg("brkTile017.wsg", &tilemap->tiles[16], false); + loadWsg("brkTile018.wsg", &tilemap->tiles[17], false); + loadWsg("brkTile019.wsg", &tilemap->tiles[18], false); + loadWsg("brkTile020.wsg", &tilemap->tiles[19], false); + loadWsg("brkTile021.wsg", &tilemap->tiles[20], false); + loadWsg("brkTile022.wsg", &tilemap->tiles[21], false); + loadWsg("brkTile023.wsg", &tilemap->tiles[22], false); + loadWsg("brkTile024.wsg", &tilemap->tiles[23], false); + loadWsg("brkTile025.wsg", &tilemap->tiles[24], false); + loadWsg("brkTile026.wsg", &tilemap->tiles[25], false); + loadWsg("brkTile027.wsg", &tilemap->tiles[26], false); + + tilemap->tiles[27] = tilemap->tiles[0]; + tilemap->tiles[28] = tilemap->tiles[0]; + tilemap->tiles[29] = tilemap->tiles[0]; + tilemap->tiles[30] = tilemap->tiles[0]; + + + loadWsg("brkTile032.wsg", &tilemap->tiles[31], false); + loadWsg("brkTile033.wsg", &tilemap->tiles[32], false); + loadWsg("brkTile034.wsg", &tilemap->tiles[33], false); + loadWsg("brkTile035.wsg", &tilemap->tiles[34], false); + loadWsg("brkTile036.wsg", &tilemap->tiles[35], false); + loadWsg("brkTile037.wsg", &tilemap->tiles[36], false); + loadWsg("brkTile038.wsg", &tilemap->tiles[37], false); + loadWsg("brkTile039.wsg", &tilemap->tiles[38], false); + loadWsg("brkTile040.wsg", &tilemap->tiles[39], false); + loadWsg("brkTile041.wsg", &tilemap->tiles[40], false); + loadWsg("brkTile042.wsg", &tilemap->tiles[41], false); + loadWsg("brkTile043.wsg", &tilemap->tiles[42], false); + loadWsg("brkTile044.wsg", &tilemap->tiles[43], false); + loadWsg("brkTile045.wsg", &tilemap->tiles[44], false); + loadWsg("brkTile046.wsg", &tilemap->tiles[45], false); + loadWsg("brkTile047.wsg", &tilemap->tiles[46], false); + loadWsg("brkTile048.wsg", &tilemap->tiles[47], false); + loadWsg("brkTile049.wsg", &tilemap->tiles[48], false); + loadWsg("brkTile050.wsg", &tilemap->tiles[49], false); + loadWsg("brkTile051.wsg", &tilemap->tiles[50], false); + loadWsg("brkTile052.wsg", &tilemap->tiles[51], false); + loadWsg("brkTile053.wsg", &tilemap->tiles[52], false); + loadWsg("brkTile054.wsg", &tilemap->tiles[53], false); + loadWsg("brkTile055.wsg", &tilemap->tiles[54], false); + /*loadWsg("brkTile056.wsg", &tilemap->tiles[55], false); + loadWsg("brkTile057.wsg", &tilemap->tiles[56], false); + loadWsg("brkTile058.wsg", &tilemap->tiles[57], false); + loadWsg("brkTile059.wsg", &tilemap->tiles[58], false); + loadWsg("brkTile060.wsg", &tilemap->tiles[59], false); + loadWsg("brkTile061.wsg", &tilemap->tiles[60], false); + loadWsg("brkTile062.wsg", &tilemap->tiles[61], false); + loadWsg("brkTile063.wsg", &tilemap->tiles[62], false);*/ + tilemap->tiles[55] = tilemap->tiles[0]; + tilemap->tiles[56] = tilemap->tiles[0]; + tilemap->tiles[57] = tilemap->tiles[0]; + tilemap->tiles[58] = tilemap->tiles[0]; + tilemap->tiles[59] = tilemap->tiles[0]; + tilemap->tiles[60] = tilemap->tiles[0]; + tilemap->tiles[61] = tilemap->tiles[0]; + tilemap->tiles[62] = tilemap->tiles[0]; + loadWsg("brkTile064.wsg", &tilemap->tiles[63], false); + loadWsg("brkTile065.wsg", &tilemap->tiles[64], false); + loadWsg("brkTile066.wsg", &tilemap->tiles[65], false); + loadWsg("brkTile067.wsg", &tilemap->tiles[66], false); + loadWsg("brkTile068.wsg", &tilemap->tiles[67], false); + loadWsg("brkTile069.wsg", &tilemap->tiles[68], false); + loadWsg("brkTile070.wsg", &tilemap->tiles[69], false); + loadWsg("brkTile071.wsg", &tilemap->tiles[70], false); + loadWsg("brkTile072.wsg", &tilemap->tiles[71], false); + loadWsg("brkTile073.wsg", &tilemap->tiles[72], false); + loadWsg("brkTile074.wsg", &tilemap->tiles[73], false); + loadWsg("brkTile075.wsg", &tilemap->tiles[74], false); + loadWsg("brkTile076.wsg", &tilemap->tiles[75], false); + loadWsg("brkTile077.wsg", &tilemap->tiles[76], false); + loadWsg("brkTile078.wsg", &tilemap->tiles[77], false); + loadWsg("brkTile079.wsg", &tilemap->tiles[78], false); + loadWsg("brkTile080.wsg", &tilemap->tiles[79], false); + loadWsg("brkTile081.wsg", &tilemap->tiles[80], false); + loadWsg("brkTile082.wsg", &tilemap->tiles[81], false); + loadWsg("brkTile083.wsg", &tilemap->tiles[82], false); + loadWsg("brkTile084.wsg", &tilemap->tiles[83], false); + loadWsg("brkTile085.wsg", &tilemap->tiles[84], false); + loadWsg("brkTile086.wsg", &tilemap->tiles[85], false); + loadWsg("brkTile087.wsg", &tilemap->tiles[86], false); + loadWsg("brkTile088.wsg", &tilemap->tiles[87], false); + loadWsg("brkTile089.wsg", &tilemap->tiles[88], false); + loadWsg("brkTile090.wsg", &tilemap->tiles[89], false); + loadWsg("brkTile091.wsg", &tilemap->tiles[90], false); + loadWsg("brkTile092.wsg", &tilemap->tiles[91], false); + loadWsg("brkTile093.wsg", &tilemap->tiles[92], false); + loadWsg("brkTile094.wsg", &tilemap->tiles[93], false); + loadWsg("brkTile095.wsg", &tilemap->tiles[94], false); + loadWsg("brkTile096.wsg", &tilemap->tiles[95], false); + loadWsg("brkTile097.wsg", &tilemap->tiles[96], false); + loadWsg("brkTile098.wsg", &tilemap->tiles[97], false); + loadWsg("brkTile099.wsg", &tilemap->tiles[98], false); + loadWsg("brkTile100.wsg", &tilemap->tiles[99], false); + loadWsg("brkTile101.wsg", &tilemap->tiles[100], false); + loadWsg("brkTile102.wsg", &tilemap->tiles[101], false); + loadWsg("brkTile103.wsg", &tilemap->tiles[102], false); + loadWsg("brkTile104.wsg", &tilemap->tiles[103], false); + loadWsg("brkTile105.wsg", &tilemap->tiles[104], false); + loadWsg("brkTile106.wsg", &tilemap->tiles[105], false); + loadWsg("brkTile107.wsg", &tilemap->tiles[106], false); + loadWsg("brkTile108.wsg", &tilemap->tiles[107], false); + loadWsg("brkTile109.wsg", &tilemap->tiles[108], false); + loadWsg("brkTile110.wsg", &tilemap->tiles[109], false); + loadWsg("brkTile111.wsg", &tilemap->tiles[110], false); + loadWsg("brkTile112.wsg", &tilemap->tiles[111], false); + loadWsg("brkTile113.wsg", &tilemap->tiles[112], false); + loadWsg("brkTile114.wsg", &tilemap->tiles[113], false); + loadWsg("brkTile115.wsg", &tilemap->tiles[114], false); + loadWsg("brkTile116.wsg", &tilemap->tiles[115], false); + loadWsg("brkTile117.wsg", &tilemap->tiles[116], false); + loadWsg("brkTile118.wsg", &tilemap->tiles[117], false); + loadWsg("brkTile119.wsg", &tilemap->tiles[118], false); + loadWsg("brkTile120.wsg", &tilemap->tiles[119], false); + loadWsg("brkTile121.wsg", &tilemap->tiles[120], false); + loadWsg("brkTile122.wsg", &tilemap->tiles[121], false); + loadWsg("brkTile123.wsg", &tilemap->tiles[122], false); + loadWsg("brkTile124.wsg", &tilemap->tiles[123], false); + loadWsg("brkTile125.wsg", &tilemap->tiles[124], false); + loadWsg("brkTile126.wsg", &tilemap->tiles[125], false); + loadWsg("brkTile127.wsg", &tilemap->tiles[126], false); + + + + + + return true; +} + +void tileSpawnEntity(tilemap_t *tilemap, uint8_t objectIndex, uint8_t tx, uint8_t ty) +{ + entity_t *entityCreated = createEntity(tilemap->entityManager, objectIndex, (tx << TILE_SIZE_IN_POWERS_OF_2) + 4, (ty << TILE_SIZE_IN_POWERS_OF_2) + 4); + + if (entityCreated != NULL) + { + entityCreated->homeTileX = tx; + entityCreated->homeTileY = ty; + tilemap->map[ty * tilemap->mapWidth + tx] = 0; + } +} + +uint8_t getTile(tilemap_t *tilemap, uint8_t tx, uint8_t ty) +{ + // ty = CLAMP(ty, 0, tilemap->mapHeight - 1); + + if (/*ty < 0 ||*/ ty >= tilemap->mapHeight) + { + ty = 0; + //return 0; + } + + if (/*tx < 0 ||*/ tx >= tilemap->mapWidth) + { + return 0; + } + + return tilemap->map[ty * tilemap->mapWidth + tx]; +} + +void setTile(tilemap_t *tilemap, uint8_t tx, uint8_t ty, uint8_t newTileId) +{ + // ty = CLAMP(ty, 0, tilemap->mapHeight - 1); + + if (ty >= tilemap->mapHeight || tx >= tilemap->mapWidth) + { + return; + } + + tilemap->map[ty * tilemap->mapWidth + tx] = newTileId; +} + +bool isSolid(uint8_t tileId) +{ + switch (tileId) + { + case TILE_EMPTY: + return false; + break; + case TILE_BOUNDARY_1 ... TILE_UNUSED_127: + return true; + break; + default: + return false; + } +} + +bool isBlock(uint8_t tileId){ + switch (tileId) + { + case TILE_BLOCK_1x1_RED ... TILE_BLOCK_2x2_BLACK_DR: + return true; + default: + return false; + } + + return false; +} + +// bool isInteractive(uint8_t tileId) +// { +// return tileId > TILE_INVISIBLE_BLOCK && tileId < TILE_BG_GOAL_ZONE; +// } + +void unlockScrolling(tilemap_t *tilemap){ + tilemap->minMapOffsetX = 0; + tilemap->maxMapOffsetX = tilemap->mapWidth * TILE_SIZE - TILEMAP_DISPLAY_WIDTH_PIXELS; + + tilemap->minMapOffsetY = 0; + tilemap->maxMapOffsetY = tilemap->mapHeight * TILE_SIZE - TILEMAP_DISPLAY_HEIGHT_PIXELS; +} + +bool needsTransparency(uint8_t tileId){ + switch(tileId) { + case TILE_BOUNDARY_1 ... TILE_UNUSED_31: + return false; + case TILE_BLOCK_2x1_RED_L ... TILE_UNUSED_127: + return true; + default: + return false; + } +} + +void freeTilemap(tilemap_t *tilemap){ + free(tilemap->map); + for(uint8_t i=0; i<127; i++){ + switch(i){ + //Skip all placeholder tiles, since they reuse other tiles + //(see loadTiles) + case 3 ... 14: + case 27 ... 30: + case 55 ... 62: + { + break; + } + default: { + freeWsg(&tilemap->tiles[i]); + break; + } + } + } + + +} \ No newline at end of file diff --git a/main/modes/breakout/tilemap.h b/main/modes/breakout/tilemap.h new file mode 100644 index 000000000..e8341041d --- /dev/null +++ b/main/modes/breakout/tilemap.h @@ -0,0 +1,217 @@ +#ifndef _TILEMAP_H_ +#define _TILEMAP_H_ + +//============================================================================== +// Includes +//============================================================================== + +#include +#include +#include "wsg.h" +#include "breakout_typedef.h" +#include "entityManager.h" + +//============================================================================== +// Constants +//============================================================================== +#define CLAMP(x, l, u) ((x) < l ? l : ((x) > u ? u : (x))) + +#define TILEMAP_DISPLAY_WIDTH_PIXELS 280 // The screen size +#define TILEMAP_DISPLAY_HEIGHT_PIXELS 240 // The screen size +#define TILEMAP_DISPLAY_WIDTH_TILES 36 // The screen size in tiles + 1 +#define TILEMAP_DISPLAY_HEIGHT_TILES 31 // The screen size in tiles + 1 + +#define TILE_SIZE 8 +#define TILE_SIZE_IN_POWERS_OF_2 3 + +#define TILESET_SIZE 128 + +//============================================================================== +// Enums +//============================================================================== +typedef enum { + TILE_EMPTY, + TILE_BOUNDARY_1, + TILE_BOUNDARY_2, + TILE_BOUNDARY_3, + TILE_UNUSED_4, + TILE_UNUSED_5, + TILE_UNUSED_6, + TILE_UNUSED_7, + TILE_UNUSED_8, + TILE_UNUSED_9, + TILE_UNUSED_A, + TILE_UNUSED_B, + TILE_UNUSED_C, + TILE_UNUSED_D, + TILE_UNUSED_E, + TILE_UNUSED_F, + TILE_BLOCK_1x1_RED, + TILE_BLOCK_1x1_ORANGE, + TILE_BLOCK_1x1_YELLOW, + TILE_BLOCK_1x1_GREEN, + TILE_BLOCK_1x1_CYAN, + TILE_BLOCK_1x1_BLUE, + TILE_BLOCK_1x1_PURPLE, + TILE_BLOCK_1x1_MAGENTA, + TILE_BLOCK_1x1_WHITE, + TILE_BLOCK_1x1_TAN, + TILE_BLOCK_1x1_BROWN, + TILE_BLOCK_1x1_BLACK, + TILE_UNUSED_28, + TILE_UNUSED_29, + TILE_UNUSED_30, + TILE_UNUSED_31, + TILE_BLOCK_2x1_RED_L, + TILE_BLOCK_2x1_RED_R, + TILE_BLOCK_2x1_ORANGE_L, + TILE_BLOCK_2x1_ORANGE_R, + TILE_BLOCK_2x1_YELLOW_L, + TILE_BLOCK_2x1_YELLOW_R, + TILE_BLOCK_2x1_GREEN_L, + TILE_BLOCK_2x1_GREEN_R, + TILE_BLOCK_2x1_CYAN_L, + TILE_BLOCK_2x1_CYAN_R, + TILE_BLOCK_2x1_BLUE_L, + TILE_BLOCK_2x1_BLUE_R, + TILE_BLOCK_2x1_PURPLE_L, + TILE_BLOCK_2x1_PURPLE_R, + TILE_BLOCK_2x1_MAGENTA_L, + TILE_BLOCK_2x1_MAGENTA_R, + TILE_BLOCK_2x1_WHITE_L, + TILE_BLOCK_2x1_WHITE_R, + TILE_BLOCK_2x1_TAN_L, + TILE_BLOCK_2x1_TAN_R, + TILE_BLOCK_2x1_BROWN_L, + TILE_BLOCK_2x1_BROWN_R, + TILE_BLOCK_2x1_BLACK_L, + TILE_BLOCK_2x1_BLACK_R, + TILE_SOLID_UNUSED_56, + TILE_SOLID_UNUSED_57, + TILE_SOLID_UNUSED_58, + TILE_SOLID_UNUSED_59, + TILE_SOLID_UNUSED_60, + TILE_SOLID_UNUSED_61, + TILE_SOLID_UNUSED_62, + TILE_SOLID_UNUSED_63, + TILE_BLOCK_2x2_RED_UL, + TILE_BLOCK_2x2_RED_UR, + TILE_BLOCK_2x2_ORANGE_UL, + TILE_BLOCK_2x2_ORANGE_UR, + TILE_BLOCK_2x2_YELLOW_UL, + TILE_BLOCK_2x2_YELLOW_UR, + TILE_BLOCK_2x2_GREEN_UL, + TILE_BLOCK_2x2_GREEN_UR, + TILE_BLOCK_2x2_CYAN_UL, + TILE_BLOCK_2x2_CYAN_UR, + TILE_BLOCK_2x2_BLUE_UL, + TILE_BLOCK_2x2_BLUE_UR, + TILE_BLOCK_2x2_PURPLE_UL, + TILE_BLOCK_2x2_PURPLE_UR, + TILE_BLOCK_2x2_MAGENTA_UL, + TILE_BLOCK_2x2_MAGENTA_UR, + TILE_BLOCK_2x2_RED_DL, + TILE_BLOCK_2x2_RED_DR, + TILE_BLOCK_2x2_ORANGE_DL, + TILE_BLOCK_2x2_ORANGE_DR, + TILE_BLOCK_2x2_YELLOW_DL, + TILE_BLOCK_2x2_YELLOW_DR, + TILE_BLOCK_2x2_GREEN_DL, + TILE_BLOCK_2x2_GREEN_DR, + TILE_BLOCK_2x2_CYAN_DL, + TILE_BLOCK_2x2_CYAN_DR, + TILE_BLOCK_2x2_BLUE_DL, + TILE_BLOCK_2x2_BLUE_DR, + TILE_BLOCK_2x2_PURPLE_DL, + TILE_BLOCK_2x2_PURPLE_DR, + TILE_BLOCK_2x2_MAGENTA_DL, + TILE_BLOCK_2x2_MAGENTA_DR, + TILE_BLOCK_2x2_WHITE_UL, + TILE_BLOCK_2x2_WHITE_UR, + TILE_BLOCK_2x2_TAN_UL, + TILE_BLOCK_2x2_TAN_UR, + TILE_BLOCK_2x2_BROWN_UL, + TILE_BLOCK_2x2_BROWN_UR, + TILE_BLOCK_2x2_BLACK_UL, + TILE_BLOCK_2x2_BLACK_UR, + TILE_UNUSED_104, + TILE_UNUSED_105, + TILE_UNUSED_106, + TILE_UNUSED_107, + TILE_UNUSED_108, + TILE_UNUSED_109, + TILE_UNUSED_110, + TILE_UNUSED_111, + TILE_BLOCK_2x2_WHITE_DL, + TILE_BLOCK_2x2_WHITE_DR, + TILE_BLOCK_2x2_TAN_DL, + TILE_BLOCK_2x2_TAN_DR, + TILE_BLOCK_2x2_BROWN_DL, + TILE_BLOCK_2x2_BROWN_DR, + TILE_BLOCK_2x2_BLACK_DL, + TILE_BLOCK_2x2_BLACK_DR, + TILE_UNUSED_120, + TILE_UNUSED_121, + TILE_UNUSED_122, + TILE_UNUSED_123, + TILE_UNUSED_124, + TILE_UNUSED_125, + TILE_UNUSED_126, + TILE_UNUSED_127 +} tileIndex_t; + +//============================================================================== +// Structs +//============================================================================== +typedef struct { + uint8_t x; + uint8_t y; +} warp_t; + struct tilemap_t +{ + wsg_t tiles[TILESET_SIZE]; + + uint8_t * map; + uint8_t mapWidth; + uint8_t mapHeight; + + uint16_t totalTargetBlocks; + + int16_t mapOffsetX; + int16_t mapOffsetY; + + int16_t minMapOffsetX; + int16_t maxMapOffsetX; + int16_t minMapOffsetY; + int16_t maxMapOffsetY; + + bool tileSpawnEnabled; + int16_t executeTileSpawnColumn; + int16_t executeTileSpawnRow; + bool executeTileSpawnAll; + + entityManager_t *entityManager; + + uint8_t animationFrame; + int16_t animationTimer; +}; + +//============================================================================== +// Prototypes +//============================================================================== +void initializeTileMap(tilemap_t * tilemap); +void drawTileMap(tilemap_t * tilemap); +void scrollTileMap(tilemap_t * tilemap, int16_t x, int16_t y); +void drawTile(tilemap_t * tilemap, uint8_t tileId, int16_t x, int16_t y); +bool loadMapFromFile(tilemap_t * tilemap, const char * name); +bool loadTiles(tilemap_t * tilemap); +void tileSpawnEntity(tilemap_t * tilemap, uint8_t objectIndex, uint8_t tx, uint8_t ty); +uint8_t getTile(tilemap_t *tilemap, uint8_t tx, uint8_t ty); +void setTile(tilemap_t *tilemap, uint8_t tx, uint8_t ty, uint8_t newTileId); +bool isSolid(uint8_t tileId); +bool isBlock(uint8_t tileId); +void unlockScrolling(tilemap_t *tilemap); +bool needsTransparency(uint8_t tileId); +void freeTilemap(tilemap_t *tilemap); + +#endif diff --git a/main/modes/dance/dance.c b/main/modes/dance/dance.c index 36273950f..581ebceeb 100644 --- a/main/modes/dance/dance.c +++ b/main/modes/dance/dance.c @@ -18,7 +18,7 @@ // Defines //============================================================================== -#define RGB_2_ARG(r, g, b) ((((r)&0xFF) << 16) | (((g)&0xFF) << 8) | (((b)&0xFF))) +#define RGB_2_ARG(r, g, b) ((((r) & 0xFF) << 16) | (((g) & 0xFF) << 8) | (((b) & 0xFF))) #define ARG_R(arg) (((arg) >> 16) & 0xFF) #define ARG_G(arg) (((arg) >> 8) & 0xFF) #define ARG_B(arg) (((arg) >> 0) & 0xFF) diff --git a/main/modes/demo/demoMode.c b/main/modes/demo/demoMode.c index bcc95f625..aafb0c495 100644 --- a/main/modes/demo/demoMode.c +++ b/main/modes/demo/demoMode.c @@ -57,6 +57,9 @@ typedef struct wsg_t king_donut; song_t ode_to_joy; p2pInfo p2p; + uint16_t sentPackets; + uint16_t recvPackets; + connectionEvt_t conStatus; menu_t* menu; } demoVars_t; @@ -95,11 +98,10 @@ static void demoEnterMode(void) addSingleItemToMenu(dv->menu, demoMenu5); addSingleItemToMenu(dv->menu, demoMenu6); - p2pInitialize(&dv->p2p, 'd', demoConCb, demoMsgRxCb, -70); - p2pStartConnection(&dv->p2p); + dv->conStatus = CON_LOST; - const uint8_t testMsg[] = {0x01, 0x02, 0x03, 0x04}; - p2pSendMsg(&dv->p2p, testMsg, ARRAY_SIZE(testMsg), demoMsgTxCbFn); + p2pInitialize(&dv->p2p, 'p', demoConCb, demoMsgRxCb, -70); + p2pStartConnection(&dv->p2p); const char demoKey[] = "demo_high_score"; int32_t highScoreToWrite = 99999; @@ -148,6 +150,13 @@ static void demoMainLoop(int64_t elapsedUs) lastBtnState = evt.state; // drawScreen = evt.down; + if (evt.button == PB_B && evt.down && dv->conStatus == CON_ESTABLISHED) + { + printf("Sending packet\n"); + const uint8_t testMsg[] = {0x01, 0x02, 0x03, 0x04}; + p2pSendMsg(&dv->p2p, testMsg, ARRAY_SIZE(testMsg), demoMsgTxCbFn); + } + static hid_gamepad_report_t report; report.buttons = lastBtnState; sendUsbGamepadReport(&report); @@ -172,6 +181,43 @@ static void demoMainLoop(int64_t elapsedUs) // Odd-even fill the rectangle with blue oddEvenFill(190, 140, 260, 230, c050, c005); + char buffer[16]; + snprintf(buffer, sizeof(buffer) - 1, "%" PRIu16, dv->sentPackets); + + fillDisplayArea(15, 200, 45, 220, c544); + drawText(&dv->ibm, c000, buffer, 20, 205); + + snprintf(buffer, sizeof(buffer) - 1, "%" PRIu16, dv->recvPackets); + fillDisplayArea(45, 200, 75, 220, c454); + drawText(&dv->ibm, c000, buffer, 50, 205); + + paletteColor_t statusColor; + switch (dv->conStatus) + { + case CON_STARTED: + statusColor = c300; + break; + + case RX_GAME_START_ACK: + statusColor = c330; + break; + + case RX_GAME_START_MSG: + statusColor = c530; + break; + + case CON_ESTABLISHED: + statusColor = c050; + break; + + case CON_LOST: + default: + statusColor = c500; + break; + } + + fillDisplayArea(15, 180, 75, 200, statusColor); + // Check for analog touch // int32_t centerVal, intensityVal; // if (getTouchCentroid(¢erVal, &intensityVal)) @@ -324,7 +370,16 @@ static int16_t demoAdvancedUSB(uint8_t* buffer, uint16_t length, uint8_t isGet) */ static void demoConCb(p2pInfo* p2p, connectionEvt_t evt) { - // Do something + if (evt == CON_ESTABLISHED) + { + if (GOING_FIRST == p2pGetPlayOrder(p2p)) + { + const uint8_t testMsg[] = {0x01, 0x02, 0x03, 0x04}; + p2pSendMsg(&dv->p2p, testMsg, ARRAY_SIZE(testMsg), demoMsgTxCbFn); + } + } + + dv->conStatus = evt; } /** @@ -337,6 +392,7 @@ static void demoConCb(p2pInfo* p2p, connectionEvt_t evt) static void demoMsgRxCb(p2pInfo* p2p, const uint8_t* payload, uint8_t len) { // Do something + dv->recvPackets++; } /** @@ -350,6 +406,7 @@ static void demoMsgRxCb(p2pInfo* p2p, const uint8_t* payload, uint8_t len) static void demoMsgTxCbFn(p2pInfo* p2p, messageStatus_t status, const uint8_t* data, uint8_t len) { // Do something + dv->sentPackets++; } /** @@ -362,4 +419,4 @@ static void demoMsgTxCbFn(p2pInfo* p2p, messageStatus_t status, const uint8_t* d static void demoMenuCb(const char* label, bool selected, uint32_t settingVal) { printf("%s %s\n", label, selected ? "selected" : "scrolled to"); -} \ No newline at end of file +} diff --git a/main/modes/gamepad/gamepad.c b/main/modes/gamepad/gamepad.c new file mode 100644 index 000000000..68f36bbf4 --- /dev/null +++ b/main/modes/gamepad/gamepad.c @@ -0,0 +1,1030 @@ +//============================================================================== +// Includes +//============================================================================== + +#include +#include + +#include "tinyusb.h" +#include "esp_timer.h" +#include "esp_log.h" + +#include "touchUtils.h" +#include "gamepad.h" +#include "mainMenu.h" + +//============================================================================== +// Defines +//============================================================================== + +#define Y_OFF 20 + +#define DPAD_BTN_RADIUS 16 +#define DPAD_CLUSTER_RADIUS 45 + +#define START_BTN_RADIUS 10 +#define START_BTN_SEP 2 + +#define AB_BTN_RADIUS 25 +#define AB_BTN_Y_OFF 8 +#define AB_BTN_SEP 2 + +#define ACCEL_BAR_HEIGHT 8 +#define ACCEL_BAR_SEP 1 +#define MAX_ACCEL_BAR_W 100 + +#define TOUCHBAR_WIDTH 100 +#define TOUCHBAR_HEIGHT 20 +#define TOUCHBAR_Y_OFF 55 +// #define TOUCHBAR_ANALOG_HEIGHT 8 + +//============================================================================== +// Enums +//============================================================================== + +typedef enum +{ + GAMEPAD_MENU, + GAMEPAD_MAIN +} gamepadScreen_t; + +typedef enum +{ + GAMEPAD_GENERIC, + GAMEPAD_NS +} gamepadType_t; + +/// Switch Gamepad Buttons Bitmap +typedef enum +{ + GAMEPAD_NS_BUTTON_Y = 0x01, + GAMEPAD_NS_BUTTON_B = 0x02, + GAMEPAD_NS_BUTTON_A = 0x04, + GAMEPAD_NS_BUTTON_X = 0x08, + GAMEPAD_NS_BUTTON_TL = 0x10, + GAMEPAD_NS_BUTTON_TR = 0x20, + GAMEPAD_NS_BUTTON_TL2 = 0x40, + GAMEPAD_NS_BUTTON_TR2 = 0x80, + GAMEPAD_NS_BUTTON_MINUS = 0x100, + GAMEPAD_NS_BUTTON_PLUS = 0x200, + GAMEPAD_NS_BUTTON_THUMBL = 0x400, + GAMEPAD_NS_BUTTON_THUMBR = 0x800, + GAMEPAD_NS_BUTTON_HOME = 0x1000, + GAMEPAD_NS_BUTTON_CAPTURE = 0x2000, + GAMEPAD_NS_BUTTON_Z = 0x4000, /// UNUSED? +} hid_gamepad_ns_button_bm_t; + +/// Switch Gamepad HAT/DPAD Buttons (from Linux input event codes) +typedef enum +{ + GAMEPAD_NS_HAT_CENTERED = 8, ///< DPAD_CENTERED + GAMEPAD_NS_HAT_UP = 0, ///< DPAD_UP + GAMEPAD_NS_HAT_UP_RIGHT = 1, ///< DPAD_UP_RIGHT + GAMEPAD_NS_HAT_RIGHT = 2, ///< DPAD_RIGHT + GAMEPAD_NS_HAT_DOWN_RIGHT = 3, ///< DPAD_DOWN_RIGHT + GAMEPAD_NS_HAT_DOWN = 4, ///< DPAD_DOWN + GAMEPAD_NS_HAT_DOWN_LEFT = 5, ///< DPAD_DOWN_LEFT + GAMEPAD_NS_HAT_LEFT = 6, ///< DPAD_LEFT + GAMEPAD_NS_HAT_UP_LEFT = 7, ///< DPAD_UP_LEFT +} hid_gamepad_ns_hat_t; + +//============================================================================== +// Structs +//============================================================================== + +/// HID Switch Gamepad Protocol Report. +typedef struct TU_ATTR_PACKED +{ + uint16_t buttons; ///< Buttons mask for currently pressed buttons + uint8_t hat; ///< Buttons mask for currently pressed buttons in the DPad/hat + int8_t x; ///< Delta x movement of left analog-stick + int8_t y; ///< Delta y movement of left analog-stick + int8_t rx; ///< Delta Rx movement of analog left trigger + int8_t ry; ///< Delta Ry movement of analog right trigger + int8_t z; ///< Delta z movement of right analog-joystick + int8_t rz; ///< Delta Rz movement of right analog-joystick +} hid_gamepad_ns_report_t; + +typedef struct // 4 bools = 4 bytes = 32 bits +{ + bool touchAnalogOn; // Least significant byte + bool accelOn; + bool _reserved1; + bool _reserved2; // Most significant byte +} gamepadToggleSettings_t; + +union gamepadToggleSettings_u +{ + int32_t i; + gamepadToggleSettings_t settings; +}; + +typedef struct +{ + font_t ibmFont; + font_t logbookFont; + + menu_t* menu; + menuLogbookRenderer_t* renderer; + gamepadScreen_t screen; + + hid_gamepad_report_t gpState; + hid_gamepad_ns_report_t gpNsState; + + uint8_t gamepadType; + bool isPluggedIn; + + union gamepadToggleSettings_u gamepadToggleSettings; +} gamepad_t; + +//============================================================================== +// Functions Prototypes +//============================================================================== + +void gamepadEnterMode(void); +void gamepadExitMode(void); +void gamepadMainLoop(int64_t elapsedUs); +void gamepadButtonCb(buttonEvt_t* evt); +void gamepadReportStateToHost(void); + +void gamepadMainMenuCb(const char* label, bool selected, uint32_t settingVal); +void gamepadMenuLoop(int64_t elapsedUs); +void gamepadStart(gamepadType_t type); + +static bool saveGamepadToggleSettings(union gamepadToggleSettings_u* toggleSettings); +static bool loadGamepadToggleSettings(union gamepadToggleSettings_u* toggleSettings); + +static const char* getButtonName(hid_gamepad_button_bm_t button); + +//============================================================================== +// Variables +//============================================================================== + +static const char str_pc[] = "PC"; +static const char str_ns[] = "Switch"; +static const char str_touch_analog_on[] = "Touch: Digi+Analog"; +static const char str_touch_analog_off[] = "Touch: Digital Only"; +static const char str_accel_on[] = "Accel: On"; +static const char str_accel_off[] = "Accel: Off"; +static const char str_exit[] = "Exit"; +static const char KEY_GAMEPAD_SETTINGS[] = "gp_settings"; + +gamepad_t* gamepad; + +swadgeMode_t gamepadMode = { + .modeName = "Gamepad", + .wifiMode = NO_WIFI, + .overrideUsb = true, + .usesAccelerometer = true, + .usesThermometer = false, + .fnEnterMode = gamepadEnterMode, + .fnExitMode = gamepadExitMode, + .fnMainLoop = gamepadMenuLoop, + .fnAudioCallback = NULL, + .fnBackgroundDrawCallback = NULL, + .fnEspNowRecvCb = NULL, + .fnEspNowSendCb = NULL, + .fnAdvancedUSB = NULL, + +}; + +// TODO: handle 8-way joystick +const hid_gamepad_button_bm_t touchMap[] = { + GAMEPAD_BUTTON_C, GAMEPAD_BUTTON_X, GAMEPAD_BUTTON_Y, GAMEPAD_BUTTON_Z, GAMEPAD_BUTTON_TL, +}; + +// TODO: handle 8-way joystick? +const hid_gamepad_button_bm_t touchMapNs[] = { + GAMEPAD_NS_BUTTON_Y, GAMEPAD_NS_BUTTON_TL, GAMEPAD_NS_BUTTON_Z, GAMEPAD_NS_BUTTON_TR, GAMEPAD_NS_BUTTON_X, +}; + +/// @brief Switch Descriptor +static const tusb_desc_device_t nsDescriptor = { + .bLength = 18U, + .bDescriptorType = 1, + .bcdUSB = 0x0200, + .bDeviceClass = 0x00, + .bDeviceSubClass = 0x00, + .bDeviceProtocol = 0x00, + .bMaxPacketSize0 = 64, + .idVendor = 0x0f0d, + .idProduct = 0x0092, + .bcdDevice = 0x0100, + .iManufacturer = 0x01, + .iProduct = 0x02, + .iSerialNumber = 0x03, + .bNumConfigurations = 0x01, +}; + +/// @brief Switch tusb configuration +static const tinyusb_config_t ns_tusb_cfg = {.descriptor = &nsDescriptor}; + +/// @brief PC string Descriptor +static const char* hid_string_descriptor[5] = { + // array of pointer to string descriptors + (char[]){0x09, 0x04}, // 0: is supported language is English (0x0409) + "Magfest", // 1: Manufacturer + "Swadge Controller", // 2: Product + "123456", // 3: Serials, should use chip ID + "Swadge HID interface", // 4: HID +}; + +/// @brief PC report Descriptor +static const uint8_t hid_report_descriptor[] = {TUD_HID_REPORT_DESC_GAMEPAD()}; + +/// @brief PC Config Descriptor +static const uint8_t hid_configuration_descriptor[] = { + TUD_CONFIG_DESCRIPTOR(1, // Configuration number + 1, // interface count + 0, // string index + (TUD_CONFIG_DESC_LEN + (CFG_TUD_HID * TUD_HID_DESC_LEN)), // total length + TUSB_DESC_CONFIG_ATT_REMOTE_WAKEUP, // attribute + 100), // power in mA + + TUD_HID_DESCRIPTOR(0, // Interface number + 4, // string index + false, // boot protocol + sizeof(hid_report_descriptor), // report descriptor len + 0x81, // EP In address + 16, // size + 10), // polling interval +}; + +/// @brief PC tusb configuration +static const tinyusb_config_t pc_tusb_cfg = { + .device_descriptor = NULL, + .string_descriptor = hid_string_descriptor, + .external_phy = false, + .configuration_descriptor = hid_configuration_descriptor, +}; + +//============================================================================== +// Functions +//============================================================================== + +/** + * Enter the gamepad mode, allocate memory, initialize USB + */ +void gamepadEnterMode(void) +{ + // Allocate and zero memory + gamepad = (gamepad_t*)calloc(1, sizeof(gamepad_t)); + + // Load the fonts + loadFont("logbook.font", &(gamepad->logbookFont), false); + loadFont("ibm_vga8.font", &(gamepad->ibmFont), false); + + // Initialize menu + gamepad->menu = initMenu(gamepadMode.modeName, gamepadMainMenuCb); + addSingleItemToMenu(gamepad->menu, str_pc); + addSingleItemToMenu(gamepad->menu, str_ns); + addSingleItemToMenu(gamepad->menu, gamepad->gamepadToggleSettings.settings.touchAnalogOn ? str_touch_analog_on + : str_touch_analog_off); + addSingleItemToMenu(gamepad->menu, gamepad->gamepadToggleSettings.settings.accelOn ? str_accel_on : str_accel_off); + addSingleItemToMenu(gamepad->menu, str_exit); + + loadGamepadToggleSettings(&gamepad->gamepadToggleSettings); + + // Initialize menu renderer + gamepad->renderer = initMenuLogbookRenderer(&gamepad->logbookFont); +} + +/** + * Exit the gamepad mode and free memory + */ +void gamepadExitMode(void) +{ + deinitMenu(gamepad->menu); + deinitMenuLogbookRenderer(gamepad->renderer); + freeFont(&(gamepad->logbookFont)); + freeFont(&(gamepad->ibmFont)); + + free(gamepad); +} + +void gamepadMainMenuCb(const char* label, bool selected, uint32_t settingVal) +{ + if (selected) + { + if (label == str_pc) + { + gamepadStart(GAMEPAD_GENERIC); + gamepad->screen = GAMEPAD_MAIN; + return; + } + else if (label == str_ns) + { + gamepadStart(GAMEPAD_NS); + gamepad->screen = GAMEPAD_MAIN; + return; + } + else if (label == str_exit) + { + // Exit to main menu + switchToSwadgeMode(&mainMenuMode); + return; + } + } + else // TODO: verify this is the proper way to use `selected` + { + bool needRedraw = false; + + if (label == str_touch_analog_on) + { + // Touch analog is on, turn it off + gamepad->gamepadToggleSettings.settings.touchAnalogOn = false; + saveGamepadToggleSettings(&gamepad->gamepadToggleSettings); + + needRedraw = true; + } + else if (label == str_touch_analog_off) + { + // Touch analog is off, turn it on + gamepad->gamepadToggleSettings.settings.touchAnalogOn = true; + saveGamepadToggleSettings(&gamepad->gamepadToggleSettings); + + needRedraw = true; + } + else if (label == str_accel_on) + { + // Accel is on, turn it off + gamepad->gamepadToggleSettings.settings.accelOn = false; + saveGamepadToggleSettings(&gamepad->gamepadToggleSettings); + + needRedraw = true; + } + else if (label == str_accel_off) + { + // Accel is off, turn it on + gamepad->gamepadToggleSettings.settings.accelOn = true; + saveGamepadToggleSettings(&gamepad->gamepadToggleSettings); + + needRedraw = true; + } + + if (needRedraw) + { + gamepad->screen = GAMEPAD_MENU; + } + } +} + +void gamepadStart(gamepadType_t type) +{ + gamepad->gamepadType = type; + + if (gamepad->gamepadType == GAMEPAD_NS) + { + initTusb(&ns_tusb_cfg, (const uint8_t*)&nsDescriptor); + } + else + { + initTusb(&pc_tusb_cfg, hid_report_descriptor); + } + + gamepad->gpNsState.x = 128; + gamepad->gpNsState.y = 128; + gamepad->gpNsState.rx = 128; + gamepad->gpNsState.ry = 128; + + led_t leds[CONFIG_NUM_LEDS]; + memset(leds, 0, sizeof(leds)); + setLeds(leds, CONFIG_NUM_LEDS); +} + +/** + * Call the appropriate main loop function for the screen being displayed + * + * @param elapsedUd Time.deltaTime + */ +void gamepadMenuLoop(int64_t elapsedUs) +{ + buttonEvt_t evt = {0}; + + switch (gamepad->screen) + { + case GAMEPAD_MENU: + { + while (checkButtonQueueWrapper(&evt)) + { + gamepad->menu = menuButton(gamepad->menu, evt); + } + drawMenuLogbook(gamepad->menu, gamepad->renderer, elapsedUs); + break; + } + case GAMEPAD_MAIN: + { + while (checkButtonQueueWrapper(&evt)) + { + gamepadButtonCb(&evt); + } + gamepadMainLoop(elapsedUs); + break; + } + // No wifi mode stuff + } +} + +/** + * Draw the gamepad state to the display when it changes + * + * @param elapsedUs unused + */ +void gamepadMainLoop(int64_t elapsedUs __attribute__((unused))) +{ + // Check if plugged in or not + if (tud_ready() != gamepad->isPluggedIn) + { + gamepad->isPluggedIn = tud_ready(); + } + + // Clear the display + fillDisplayArea(0, 0, TFT_WIDTH, TFT_HEIGHT, c213); + + // Always Draw some reminder text, centered + const char reminderText[] = "Start + Select to Exit"; + int16_t tWidth = textWidth(&gamepad->ibmFont, reminderText); + drawText(&gamepad->ibmFont, c555, reminderText, (TFT_WIDTH - tWidth) / 2, 10); + + if (gamepad->gamepadType == GAMEPAD_NS) + { + // Draw button combo text, centered + const char captureText[] = "Down + Select: Capture"; + tWidth = textWidth(&gamepad->ibmFont, captureText); + int16_t textX = (TFT_WIDTH - tWidth) / 2; + int16_t afterText + = drawText(&gamepad->ibmFont, c555, captureText, textX, TFT_HEIGHT - gamepad->ibmFont.height * 2 - 12); + + const char homeText1[] = "Down + Start:"; + drawText(&gamepad->ibmFont, c555, homeText1, textX, TFT_HEIGHT - gamepad->ibmFont.height - 10); + + const char* homeText2 = getButtonName(GAMEPAD_NS_BUTTON_HOME); + tWidth = textWidth(&gamepad->ibmFont, homeText2); + drawText(&gamepad->ibmFont, c555, homeText2, afterText - tWidth - 1, TFT_HEIGHT - gamepad->ibmFont.height - 10); + } + + // If it's plugged in, draw buttons + if (gamepad->isPluggedIn) + { + // Helper function pointer + void (*drawFunc)(int, int, int, paletteColor_t); + + // A list of all the hat directions, in order + static const uint8_t hatDirs[] = { + GAMEPAD_HAT_UP, GAMEPAD_HAT_UP_RIGHT, GAMEPAD_HAT_RIGHT, GAMEPAD_HAT_DOWN_RIGHT, + GAMEPAD_HAT_DOWN, GAMEPAD_HAT_DOWN_LEFT, GAMEPAD_HAT_LEFT, GAMEPAD_HAT_UP_LEFT, + }; + + // For each hat direction + for (uint8_t i = 0; i < ARRAY_SIZE(hatDirs); i++) + { + // The degree around the cluster + int16_t deg = i * 45; + // The center of the cluster + int16_t xc = TFT_WIDTH / 4; + int16_t yc = (TFT_HEIGHT / 2) + Y_OFF; + // Draw the button around the cluster + xc += ((getSin1024(deg) * DPAD_CLUSTER_RADIUS) / 1024); + yc += ((-getCos1024(deg) * DPAD_CLUSTER_RADIUS) / 1024); + + // Draw either a filled or outline circle, if this is the direction pressed + switch (gamepad->gamepadType) + { + case GAMEPAD_NS: + { + drawFunc = (gamepad->gpNsState.hat == (hatDirs[i] - 1)) ? &drawCircleFilled : &drawCircle; + break; + } + case GAMEPAD_GENERIC: + default: + { + drawFunc = (gamepad->gpState.hat == hatDirs[i]) ? &drawCircleFilled : &drawCircle; + break; + } + } + + drawFunc(xc, yc, DPAD_BTN_RADIUS, c551 /*paletteHsvToHex(i * 32, 0xFF, 0xFF)*/); + } + + // Select button + switch (gamepad->gamepadType) + { + case GAMEPAD_NS: + { + drawFunc = (gamepad->gpNsState.buttons & GAMEPAD_NS_BUTTON_MINUS) ? &drawCircleFilled : &drawCircle; + break; + } + case GAMEPAD_GENERIC: + default: + { + drawFunc = (gamepad->gpState.buttons & GAMEPAD_BUTTON_SELECT) ? &drawCircleFilled : &drawCircle; + break; + } + } + int16_t x = (TFT_WIDTH / 2) - START_BTN_RADIUS - START_BTN_SEP; + int16_t y = (TFT_HEIGHT / 4) + Y_OFF; + drawFunc(x, y, START_BTN_RADIUS, c333); + + if (gamepad->gamepadType == GAMEPAD_NS) + { + const char* buttonName = getButtonName(GAMEPAD_NS_BUTTON_MINUS); + drawText(&gamepad->ibmFont, c444, buttonName, x - textWidth(&gamepad->ibmFont, buttonName) / 2, + y - gamepad->ibmFont.height / 2); + } + + // Start button + switch (gamepad->gamepadType) + { + case GAMEPAD_NS: + { + drawFunc = (gamepad->gpNsState.buttons & GAMEPAD_NS_BUTTON_PLUS) ? &drawCircleFilled : &drawCircle; + break; + } + case GAMEPAD_GENERIC: + default: + { + drawFunc = (gamepad->gpState.buttons & GAMEPAD_BUTTON_START) ? &drawCircleFilled : &drawCircle; + break; + } + } + x = (TFT_WIDTH / 2) + START_BTN_RADIUS + START_BTN_SEP; + drawFunc(x, y, START_BTN_RADIUS, c333); + + if (gamepad->gamepadType == GAMEPAD_NS) + { + const char* buttonName = getButtonName(GAMEPAD_NS_BUTTON_PLUS); + drawText(&gamepad->ibmFont, c444, buttonName, x - textWidth(&gamepad->ibmFont, buttonName) / 2, + y - gamepad->ibmFont.height / 2); + } + + // Button A + switch (gamepad->gamepadType) + { + case GAMEPAD_NS: + { + drawFunc = (gamepad->gpNsState.buttons & GAMEPAD_NS_BUTTON_A) ? &drawCircleFilled : &drawCircle; + break; + } + case GAMEPAD_GENERIC: + default: + { + drawFunc = (gamepad->gpState.buttons & GAMEPAD_BUTTON_A) ? &drawCircleFilled : &drawCircle; + break; + } + } + drawFunc(((3 * TFT_WIDTH) / 4) + AB_BTN_RADIUS + AB_BTN_SEP, (TFT_HEIGHT / 2) - AB_BTN_Y_OFF + Y_OFF, + AB_BTN_RADIUS, c243); + + // Button B + switch (gamepad->gamepadType) + { + case GAMEPAD_NS: + { + drawFunc = (gamepad->gpNsState.buttons & GAMEPAD_NS_BUTTON_B) ? &drawCircleFilled : &drawCircle; + break; + } + case GAMEPAD_GENERIC: + default: + { + drawFunc = (gamepad->gpState.buttons & GAMEPAD_BUTTON_B) ? &drawCircleFilled : &drawCircle; + break; + } + } + drawFunc(((3 * TFT_WIDTH) / 4) - AB_BTN_RADIUS - AB_BTN_SEP, (TFT_HEIGHT / 2) + AB_BTN_Y_OFF + Y_OFF, + AB_BTN_RADIUS, c401); + + // Draw touch strip + int16_t tBarX = TFT_WIDTH - TOUCHBAR_WIDTH; + + // TODO: translate new touchpad data into 4-way or 8-way joystick + // switch(gamepad->gamepadType){ + // case GAMEPAD_GENERIC: { + // if(evt->down) + // { + // gamepad->gpState.buttons |= touchMap[evt->pad]; + // } + // else + // { + // gamepad->gpState.buttons &= ~touchMap[evt->pad]; + // } + + // break; + // } + // case GAMEPAD_NS: { + // if(evt->down) + // { + // gamepad->gpNsState.buttons |= touchMapNs[evt->pad]; + // } + // else + // { + // gamepad->gpNsState.buttons &= ~touchMapNs[evt->pad]; + // } + + // break; + // } + // } + + // If we're on the generic gamepad and touch analog is enabled, plot the extra indicator on the screen + if (gamepad->gamepadType == GAMEPAD_GENERIC && gamepad->gamepadToggleSettings.settings.touchAnalogOn) + { + int32_t phi, r, intensity; + if (getTouchJoystick(&phi, &r, &intensity)) + { + // TODO: rebuild this for new touchpad + } + + // drawRect( + // tBarX - 1 , TOUCHBAR_Y_OFF + TOUCHBAR_HEIGHT - 1, + // TFT_WIDTH, TOUCHBAR_Y_OFF + TOUCHBAR_HEIGHT + TOUCHBAR_ANALOG_HEIGHT + 1, + // c111); + // fillDisplayArea( + // tBarX + center - 1, TOUCHBAR_Y_OFF + TOUCHBAR_HEIGHT, + // tBarX + center + 1, TOUCHBAR_Y_OFF + TOUCHBAR_HEIGHT + TOUCHBAR_ANALOG_HEIGHT, + // c444); + } + + uint8_t numTouchElem = ARRAY_SIZE(touchMap); + for (uint8_t touchIdx = 0; touchIdx < numTouchElem; touchIdx++) + { + int16_t x1 = tBarX - 1; + int16_t x2 = tBarX + (TOUCHBAR_WIDTH / numTouchElem); + + if ((gamepad->gamepadType == GAMEPAD_GENERIC) ? gamepad->gpState.buttons & touchMap[touchIdx] + : gamepad->gpNsState.buttons & touchMapNs[touchIdx]) + { + fillDisplayArea(x1, TOUCHBAR_Y_OFF, x2, TOUCHBAR_Y_OFF + TOUCHBAR_HEIGHT, c111); + } + else + { + drawRect(x1, TOUCHBAR_Y_OFF, x2, TOUCHBAR_Y_OFF + TOUCHBAR_HEIGHT, c111); + } + + if (gamepad->gamepadType == GAMEPAD_NS) + { + const char* buttonName = getButtonName(touchMapNs[touchIdx]); + drawText(&gamepad->ibmFont, c444, buttonName, + x1 + (x2 - x1 - textWidth(&gamepad->ibmFont, buttonName)) / 2, + TOUCHBAR_Y_OFF + (TOUCHBAR_HEIGHT - gamepad->ibmFont.height) / 2); + } + + tBarX += (TOUCHBAR_WIDTH / numTouchElem); + } + + if (gamepad->gamepadToggleSettings.settings.accelOn && gamepad->gamepadType == GAMEPAD_GENERIC) + { + // Declare variables to receive acceleration + int16_t a_x, a_y, a_z; + // Get the current acceleration + if (ESP_OK == accelGetAccelVec(&a_x, &a_y, &a_z)) + { + // Values are roughly -256 to 256, so divide, clamp, and save + gamepad->gpState.rx = CLAMP((a_x) / 2, -128, 127); + gamepad->gpState.ry = CLAMP((a_y) / 2, -128, 127); + gamepad->gpState.rz = CLAMP((a_z) / 2, -128, 127); + } + + // Set up drawing accel bars + int16_t barY = (TFT_HEIGHT * 3) / 4; + + // Plot X accel + int16_t barWidth = ((gamepad->gpState.rx + 128) * MAX_ACCEL_BAR_W) / 256; + fillDisplayArea(TFT_WIDTH - barWidth, barY, TFT_WIDTH, barY + ACCEL_BAR_HEIGHT, c500); + barY += (ACCEL_BAR_HEIGHT + ACCEL_BAR_SEP); + + // Plot Y accel + barWidth = ((gamepad->gpState.ry + 128) * MAX_ACCEL_BAR_W) / 256; + fillDisplayArea(TFT_WIDTH - barWidth, barY, TFT_WIDTH, barY + ACCEL_BAR_HEIGHT, c050); + barY += (ACCEL_BAR_HEIGHT + ACCEL_BAR_SEP); + + // Plot Z accel + barWidth = ((gamepad->gpState.rz + 128) * MAX_ACCEL_BAR_W) / 256; + fillDisplayArea(TFT_WIDTH - barWidth, barY, TFT_WIDTH, barY + ACCEL_BAR_HEIGHT, c005); + // barY += (ACCEL_BAR_HEIGHT + ACCEL_BAR_SEP); + } + + // Send state to host + gamepadReportStateToHost(); + } + else + { + // If it's not plugged in, give a hint + const char* plugInText; + switch (gamepad->gamepadType) + { + case GAMEPAD_NS: + { + plugInText = "Plug USB-C into Switch please!"; + break; + } + case GAMEPAD_GENERIC: + default: + { + plugInText = "Plug USB-C into computer please!"; + break; + } + } + tWidth = textWidth(&gamepad->ibmFont, plugInText); + drawText(&gamepad->ibmFont, c555, plugInText, (TFT_WIDTH - tWidth) / 2, + (TFT_HEIGHT - gamepad->ibmFont.height) / 2); + } +} + +/** + * Button callback. Send the button state over USB and save it for drawing + * + * @param evt The button event that occurred + */ +void gamepadButtonCb(buttonEvt_t* evt) +{ + switch (gamepad->gamepadType) + { + case GAMEPAD_GENERIC: + { + // Build a list of all independent buttons held down + gamepad->gpState.buttons + &= ~(GAMEPAD_BUTTON_A | GAMEPAD_BUTTON_B | GAMEPAD_BUTTON_START | GAMEPAD_BUTTON_SELECT); + if (evt->state & PB_A) + { + gamepad->gpState.buttons |= GAMEPAD_BUTTON_A; + } + if (evt->state & PB_B) + { + gamepad->gpState.buttons |= GAMEPAD_BUTTON_B; + } + if (evt->state & PB_START) + { + gamepad->gpState.buttons |= GAMEPAD_BUTTON_START; + } + if (evt->state & PB_SELECT) + { + gamepad->gpState.buttons |= GAMEPAD_BUTTON_SELECT; + } + + // Figure out which way the D-Pad is pointing + gamepad->gpState.hat = GAMEPAD_HAT_CENTERED; + if (evt->state & PB_UP) + { + if (evt->state & PB_RIGHT) + { + gamepad->gpState.hat = GAMEPAD_HAT_UP_RIGHT; + } + else if (evt->state & PB_LEFT) + { + gamepad->gpState.hat = GAMEPAD_HAT_UP_LEFT; + } + else + { + gamepad->gpState.hat = GAMEPAD_HAT_UP; + } + } + else if (evt->state & PB_DOWN) + { + if (evt->state & PB_RIGHT) + { + gamepad->gpState.hat = GAMEPAD_HAT_DOWN_RIGHT; + } + else if (evt->state & PB_LEFT) + { + gamepad->gpState.hat = GAMEPAD_HAT_DOWN_LEFT; + } + else + { + gamepad->gpState.hat = GAMEPAD_HAT_DOWN; + } + } + else if (evt->state & PB_RIGHT) + { + gamepad->gpState.hat = GAMEPAD_HAT_RIGHT; + } + else if (evt->state & PB_LEFT) + { + gamepad->gpState.hat = GAMEPAD_HAT_LEFT; + } + + break; + } + case GAMEPAD_NS: + { + // Build a list of all independent buttons held down + gamepad->gpNsState.buttons = 0; + + if (evt->state & PB_A) + { + gamepad->gpNsState.buttons |= GAMEPAD_NS_BUTTON_A; + } + if (evt->state & PB_B) + { + gamepad->gpNsState.buttons |= GAMEPAD_NS_BUTTON_B; + } + if (evt->state & PB_START) + { + if (evt->state & PB_DOWN) + { + gamepad->gpNsState.buttons |= GAMEPAD_NS_BUTTON_HOME; + } + else + { + gamepad->gpNsState.buttons |= GAMEPAD_NS_BUTTON_PLUS; + } + } + if (evt->state & PB_SELECT) + { + if (evt->state & PB_DOWN) + { + gamepad->gpNsState.buttons |= GAMEPAD_NS_BUTTON_CAPTURE; + } + else + { + gamepad->gpNsState.buttons |= GAMEPAD_NS_BUTTON_MINUS; + } + } + + // Figure out which way the D-Pad is pointing + gamepad->gpNsState.hat = GAMEPAD_NS_HAT_CENTERED; + if (evt->state & PB_UP) + { + if (evt->state & PB_RIGHT) + { + gamepad->gpNsState.hat = GAMEPAD_NS_HAT_UP_RIGHT; + } + else if (evt->state & PB_LEFT) + { + gamepad->gpNsState.hat = GAMEPAD_NS_HAT_UP_LEFT; + } + else + { + gamepad->gpNsState.hat = GAMEPAD_NS_HAT_UP; + } + } + else if (evt->state & PB_DOWN) + { + if (evt->state & PB_RIGHT) + { + gamepad->gpNsState.hat = GAMEPAD_NS_HAT_DOWN_RIGHT; + } + else if (evt->state & PB_LEFT) + { + gamepad->gpNsState.hat = GAMEPAD_NS_HAT_DOWN_LEFT; + } + else + { + gamepad->gpNsState.hat = GAMEPAD_NS_HAT_DOWN; + } + } + else if (evt->state & PB_RIGHT) + { + gamepad->gpNsState.hat = GAMEPAD_NS_HAT_RIGHT; + } + else if (evt->state & PB_LEFT) + { + gamepad->gpNsState.hat = GAMEPAD_NS_HAT_LEFT; + } + + break; + } + } + + // Send state to host + gamepadReportStateToHost(); +} + +/** + * @brief Send the state over USB to the host + */ +void gamepadReportStateToHost(void) +{ + // Only send data if USB is ready + if (tud_ready()) + { + switch (gamepad->gamepadType) + { + case GAMEPAD_GENERIC: + { + // TODO: handle 8-way joystick + if (gamepad->gamepadToggleSettings.settings.touchAnalogOn + && gamepad->gpState.buttons + & ((touchMap[0] | touchMap[1] | touchMap[2] + | touchMap[3]))) // | touchMap[4] | touchMap[5] | touchMap[6] | touchMap[7]))) + { + int32_t phi, r, intensity; + if (getTouchJoystick(&phi, &r, &intensity)) + { + int32_t x, y; + getTouchCartesian(phi, r, &x, &y); + gamepad->gpState.z = (127 * (phi - 180)) / 360; + } + else + { + gamepad->gpState.z = 0; + } + } + else + { + gamepad->gpState.z = 0; + } + // Send the state over USB + tud_hid_gamepad_report(HID_ITF_PROTOCOL_NONE, gamepad->gpState.x, gamepad->gpState.y, + gamepad->gpState.z, gamepad->gpState.rx, gamepad->gpState.ry, + gamepad->gpState.rz, gamepad->gpState.hat, gamepad->gpState.buttons); + + break; + } + case GAMEPAD_NS: + { + // TODO check this + // tud_gamepad_ns_report(&gamepad->gpNsState); + tud_hid_gamepad_report(HID_ITF_PROTOCOL_NONE, gamepad->gpState.x, gamepad->gpState.y, + gamepad->gpState.z, gamepad->gpState.rx, gamepad->gpState.ry, + gamepad->gpState.rz, gamepad->gpState.hat, gamepad->gpState.buttons); + + break; + } + } + } +} + +static bool saveGamepadToggleSettings(union gamepadToggleSettings_u* toggleSettings) +{ + return writeNvs32(KEY_GAMEPAD_SETTINGS, toggleSettings->i); +} + +static bool loadGamepadToggleSettings(union gamepadToggleSettings_u* toggleSettings) +{ + bool r = readNvs32(KEY_GAMEPAD_SETTINGS, &toggleSettings->i); + if (!r) + { + memset(toggleSettings, 0, sizeof(union gamepadToggleSettings_u)); + toggleSettings->settings.accelOn = true; + toggleSettings->settings.touchAnalogOn = true; + return saveGamepadToggleSettings(toggleSettings); + } + return true; +} + +static const char* getButtonName(hid_gamepad_button_bm_t button) +{ + switch (button) + { + case GAMEPAD_NS_BUTTON_Y: + { + return "Y"; + } + case GAMEPAD_NS_BUTTON_B: + { + return "B"; + } + case GAMEPAD_NS_BUTTON_A: + { + return "A"; + } + case GAMEPAD_NS_BUTTON_X: + { + return "X"; + } + case GAMEPAD_NS_BUTTON_TL: + { + return "L"; + } + case GAMEPAD_NS_BUTTON_TR: + { + return "R"; + } + case GAMEPAD_NS_BUTTON_TL2: + { + return "ZL"; + } + case GAMEPAD_NS_BUTTON_TR2: + { + return "ZR"; + } + case GAMEPAD_NS_BUTTON_MINUS: + { + return "-"; + } + case GAMEPAD_NS_BUTTON_PLUS: + { + return "+"; + } + case GAMEPAD_NS_BUTTON_HOME: + { + return "HOME"; + } + case GAMEPAD_NS_BUTTON_CAPTURE: + { + return "Capture"; + } + case GAMEPAD_NS_BUTTON_THUMBL: + { + return "Left Stick"; + } + case GAMEPAD_NS_BUTTON_THUMBR: + { + return "Right Stick"; + } + case GAMEPAD_NS_BUTTON_Z: + default: + { + return ""; + } + } +} diff --git a/main/modes/gamepad/gamepad.h b/main/modes/gamepad/gamepad.h new file mode 100644 index 000000000..76648e9fd --- /dev/null +++ b/main/modes/gamepad/gamepad.h @@ -0,0 +1,8 @@ +#ifndef _GAMEPAD_H_ +#define _GAMEPAD_H_ + +#include "swadge2024.h" + +extern swadgeMode_t gamepadMode; + +#endif \ No newline at end of file diff --git a/main/modes/lumberjack/lumberjack.c b/main/modes/lumberjack/lumberjack.c new file mode 100644 index 000000000..802843958 --- /dev/null +++ b/main/modes/lumberjack/lumberjack.c @@ -0,0 +1,347 @@ +//============================================================================== +// Includes +//============================================================================== + +#include +#include + +#include "menu.h" + +// For lumberjack +#include "lumberjack.h" +#include "lumberjackGame.h" + +static void lumberjackEnterMode(void); +static void lumberjackExitMode(void); +static void lumberjackMainLoop(int64_t elapsedUs); +static void lumberjackMenuLoop(int64_t elapsedUs); +static void lumberjackAudioCallback(uint16_t* samples, uint32_t sampleCnt); +static void lumberjackBackgroundDrawCallback(int16_t x, int16_t y, int16_t w, int16_t h, int16_t up, int16_t upNum); +static void lumberjackEspNowRecvCb(const esp_now_recv_info_t* esp_now_info, const uint8_t* data, uint8_t len, + int8_t rssi); +static void lumberjackEspNowSendCb(const uint8_t* mac_addr, esp_now_send_status_t status); + +static void lumberjackConCb(p2pInfo* p2p, connectionEvt_t evt); +static void lumberjackMsgRxCb(p2pInfo* p2p, const uint8_t* payload, uint8_t len); +static void lumberjackMsgTxCbFn(p2pInfo* p2p, messageStatus_t status, const uint8_t* data, uint8_t len); + +static void lumberjackMenuCb(const char*, bool selected, uint32_t settingVal); + +static void lumberjackJoinGame(void); + +static const char lumberjackName[] = "Lumberjack"; +static const char lumberjackPanic[] = "Panic"; +static const char lumberjackAttack[] = "Attack"; +static const char lumberjackBack[] = "Back"; + +// static const char lumberjackNone[] = "None"; +static const char lumberjackRedCharacter[] = "Character: Red"; +static const char lumberjackGreen[] = "Character: Green"; +static const char lumberjackSpecialCharacter[] = "Character: Special"; + +static const char lumberjackMenuSinglePlayer[] = "Single Player"; +static const char lumberjackMenuMultiPlayerHost[] = "Multi-Player"; +static const char lumberjackMenuMultiPlayerClient[] = "Multi-Player Join"; + +const char* LUM_TAG = "LUM"; + +swadgeMode_t lumberjackMode = { + .modeName = lumberjackName, + .wifiMode = ESP_NOW_IMMEDIATE, + .overrideUsb = false, + .usesAccelerometer = false, + .usesThermometer = false, + .fnEnterMode = lumberjackEnterMode, + .fnExitMode = lumberjackExitMode, + .fnMainLoop = lumberjackMainLoop, + .fnAudioCallback = lumberjackAudioCallback, + .fnBackgroundDrawCallback = lumberjackBackgroundDrawCallback, + .fnEspNowRecvCb = lumberjackEspNowRecvCb, + .fnEspNowSendCb = lumberjackEspNowSendCb, + .fnAdvancedUSB = NULL, +}; + +lumberjack_t* lumberjack = NULL; + +static void lumberjackEnterMode(void) +{ + ESP_LOGI(LUM_TAG, "Lumber!"); + // Allocate and clear all memory for the menu mode. + lumberjack = calloc(1, sizeof(lumberjack_t)); + + loadFont("ibm_vga8.font", &lumberjack->ibm, false); + loadFont("logbook.font", &lumberjack->logbook, false); + + lumberjack->menu = initMenu(lumberjackName, lumberjackMenuCb); + lumberjack->menuLogbookRenderer = initMenuLogbookRenderer(&lumberjack->logbook); + + lumberjack->gameMode = LUMBERJACK_MODE_NONE; + lumberjack->networked = false; + lumberjack->host = false; + + lumberjack->menu = startSubMenu(lumberjack->menu, lumberjackPanic); + addSingleItemToMenu(lumberjack->menu, lumberjackMenuSinglePlayer); + addSingleItemToMenu(lumberjack->menu, lumberjackMenuMultiPlayerHost); + addSingleItemToMenu(lumberjack->menu, lumberjackMenuMultiPlayerClient); + lumberjack->menu = endSubMenu(lumberjack->menu); + + addSingleItemToMenu(lumberjack->menu, lumberjackAttack); + + if (true) // Ignore this line + { + static const char* defaultCharacters[] = {lumberjackRedCharacter, lumberjackGreen}; + + addMultiItemToMenu(lumberjack->menu, defaultCharacters, ARRAY_SIZE(defaultCharacters), 0); + } + else + { + static const char* defaultCharacterswUnlocks[] + = {lumberjackRedCharacter, lumberjackGreen, lumberjackSpecialCharacter}; + + addMultiItemToMenu(lumberjack->menu, defaultCharacterswUnlocks, ARRAY_SIZE(defaultCharacterswUnlocks), 0); + } + + lumberjack->screen = LUMBERJACK_MENU; + + // Lumberjack. Game 19 + // Init menu :( + + bzrStop(); // Stop the buzzer? + + // High score stuff? + // Unlockables ? Save data? +} + +static void lumberjackJoinGame(void) +{ + if (lumberjack->gameMode == LUMBERJACK_MODE_PANIC) + { + lumberjack->screen = LUMBERJACK_A; + lumberjackStartGameMode(lumberjack, lumberjack->selected); + return; + } + + if (lumberjack->gameMode == LUMBERJACK_MODE_ATTACK) + { + lumberjack->screen = LUMBERJACK_B; + lumberjackStartGameMode(lumberjack, lumberjack->selected); + return; + } + + lumberjack->screen = LUMBERJACK_MENU; +} + +static void lumberjackExitMode(void) +{ + lumberjackExitGameMode(); + + p2pDeinit(&lumberjack->p2p); + freeFont(&lumberjack->ibm); + freeFont(&lumberjack->logbook); + deinitMenu(lumberjack->menu); + free(lumberjack); +} + +static void lumberjackMainLoop(int64_t elapsedUs) +{ + switch (lumberjack->screen) + { + case LUMBERJACK_MENU: + { + lumberjackMenuLoop(elapsedUs); + break; + } + case LUMBERJACK_A: + case LUMBERJACK_B: + { + lumberjackGameLoop(elapsedUs); + break; + } + } +} + +static void lumberjackMenuLoop(int64_t elapsedUs) +{ + buttonEvt_t evt = {0}; + while (checkButtonQueueWrapper(&evt)) + { + lumberjack->menu = menuButton(lumberjack->menu, evt); + } + + drawMenuLogbook(lumberjack->menu, lumberjack->menuLogbookRenderer, elapsedUs); +} + +static void lumberjackAudioCallback(uint16_t* samples, uint32_t sampleCnt) +{ +} + +static void lumberjackBackgroundDrawCallback(int16_t x, int16_t y, int16_t w, int16_t h, int16_t up, int16_t upNum) +{ + fillDisplayArea(x, y, x + w, y + h, c145); + + // Are we drawing the game here? +} + +//============================================================================== +// ESP_NOW +//============================================================================== +static void lumberjackEspNowRecvCb(const esp_now_recv_info_t* esp_now_info, const uint8_t* data, uint8_t len, + int8_t rssi) +{ + // ESP_LOGI(LUM_TAG, "Getting: %d", (uint8_t)&data); + p2pRecvCb(&lumberjack->p2p, esp_now_info->src_addr, data, len, rssi); +} + +static void lumberjackEspNowSendCb(const uint8_t* mac_addr, esp_now_send_status_t status) +{ + p2pSendCb(&lumberjack->p2p, mac_addr, status); +} + +static void lumberjackConCb(p2pInfo* p2p, connectionEvt_t evt) +{ + // Do anything + if (evt == CON_ESTABLISHED) + { + ESP_LOGI(LUM_TAG, "LumberJack.Net ready! %d", (int)p2pGetPlayOrder(p2p)); + + if (GOING_FIRST == p2pGetPlayOrder(p2p)) + { + ESP_LOGI(LUM_TAG, "HOST?"); + const uint8_t testMsg[] = {0x01, 0x02, 0x03, 0x04}; + + p2pSendMsg(&lumberjack->p2p, testMsg, ARRAY_SIZE(testMsg), lumberjackMsgTxCbFn); + } + } + + if (evt == CON_LOST) + { + // Do we attempt to get it back? + ESP_LOGW(LUM_TAG, "We lost connection!"); + } + + lumberjack->conStatus = evt; +} + +static void lumberjackMsgRxCb(p2pInfo* p2p, const uint8_t* payload, uint8_t len) +{ + // Do anything + + if (len > 1) + { + if (payload[0] == 0x19) + { + int locX = (int)payload[1] << 0 | (uint32_t)payload[2] << 8; + int locY = (int)payload[3] << 0 | (uint32_t)payload[4] << 8; + uint8_t frame = (uint8_t)payload[5]; + printf("Got %d,%d %d|", locX, locY, frame); + + lumberjackUpdateRemote(locX, locY, frame); + } + } + + printf("Received %d %d!\n", *payload, len); +} + +void lumberjackSendAttack(int number) +{ + const uint8_t testMsg[] = {0x13}; + p2pSendMsg(&lumberjack->p2p, testMsg, ARRAY_SIZE(testMsg), lumberjackMsgTxCbFn); +} + +void lumberjackUpdateLocation(int ghostX, int ghostY, int frame) +{ + const uint8_t locationMessage[6] + = {0x19, (uint8_t)(ghostX >> 0), (uint8_t)(ghostX >> 8), (uint8_t)(ghostY >> 0), (uint8_t)(ghostY >> 8), frame}; + p2pSendMsg(&lumberjack->p2p, locationMessage, ARRAY_SIZE(locationMessage), lumberjackMsgTxCbFn); +} + +/** + * @brief TODO use this somewhere + * + * @param p2p + * @param status + * @param data + * @param len + */ +static void lumberjackMsgTxCbFn(p2pInfo* p2p, messageStatus_t status, const uint8_t* data, uint8_t len) +{ +} + +static void lumberjackMenuCb(const char* label, bool selected, uint32_t settingVal) +{ + ESP_LOGI(LUM_TAG, "Info "); + if (selected) + { + if (label == lumberjackPanic) + { + ESP_LOGI(LUM_TAG, "Panic"); + lumberjack->gameMode = LUMBERJACK_MODE_PANIC; + // lumberjack->screen = LUMBERJACK_A; + // lumberjackStartGameMode(LUMBERJACK_MODE_PANIC, lumberjack->selected); + } + else if (label == lumberjackAttack) + { + ESP_LOGI(LUM_TAG, "Attack"); + lumberjack->gameMode = LUMBERJACK_MODE_ATTACK; + + // lumberjack->screen = LUMBERJACK_B; + // lumberjackStartGameMode(LUMBERJACK_MODE_ATTACK, lumberjack->selected); + } + + if (label == lumberjackMenuMultiPlayerHost) + { + p2pInitialize(&lumberjack->p2p, 0x13, lumberjackConCb, lumberjackMsgRxCb, -70); + p2pStartConnection(&lumberjack->p2p); + lumberjack->networked = true; + lumberjack->host = true; + lumberjackJoinGame(); + } + else if (label == lumberjackMenuMultiPlayerClient) + { + p2pInitialize(&lumberjack->p2p, 0x13, lumberjackConCb, lumberjackMsgRxCb, -70); + p2pStartConnection(&lumberjack->p2p); + lumberjack->networked = true; + lumberjack->host = false; + lumberjackJoinGame(); + } + else if (label == lumberjackMenuSinglePlayer) + { + lumberjack->networked = false; + lumberjack->host = false; + lumberjackJoinGame(); + } + + if (label == lumberjackRedCharacter) + { + lumberjack->selected = 0; + } + else if (label == lumberjackGreen) + { + lumberjack->selected = 1; + } + else if (label == lumberjackSpecialCharacter) + { + lumberjack->selected = 2; + } + + if (label == lumberjackBack) + { + //.switchToSwadgeMode(&mainMenuMode); + } + } + else + { + if (label == lumberjackRedCharacter) + { + lumberjack->selected = 0; + } + else if (label == lumberjackGreen) + { + lumberjack->selected = 1; + } + else if (label == lumberjackSpecialCharacter) + { + lumberjack->selected = 2; + } + } +} \ No newline at end of file diff --git a/main/modes/lumberjack/lumberjack.h b/main/modes/lumberjack/lumberjack.h new file mode 100644 index 000000000..e3b2655bf --- /dev/null +++ b/main/modes/lumberjack/lumberjack.h @@ -0,0 +1,99 @@ +#ifndef _LUMBERJACK_MODE_H_ +#define _LUMBERJACK_MODE_H_ + +#include "swadge2024.h" + +#include "lumberjackEntity.h" +#include "lumberjackPlayer.h" + +extern const char* LUM_TAG; +extern swadgeMode_t lumberjackMode; + +typedef enum +{ + LUMBERJACK_MENU, + LUMBERJACK_A, + LUMBERJACK_B, +} lumberjackScreen_t; + +typedef enum +{ + LUMBERJACK_MODE_NONE, + LUMBERJACK_MODE_PANIC, + LUMBERJACK_MODE_ATTACK +} lumberjackGameType_t; + +typedef struct +{ + menu_t* menu; + menuLogbookRenderer_t* menuLogbookRenderer; + font_t ibm; + font_t logbook; + + uint8_t selected; + bool networked; + bool host; + + // The pass throughs + p2pInfo p2p; + connectionEvt_t conStatus; + lumberjackScreen_t screen; + lumberjackGameType_t gameMode; + +} lumberjack_t; + +typedef struct +{ + /* data */ + int x; + int y; + int collision; + int type; + int index; + int offset; + int offset_time; + +} lumberjackTile_t; + +typedef struct +{ + bool loaded; + font_t ibm; + lumberjack_t* lumberjackMain; + menu_t* menu; + uint16_t btnState; ///<-- The STOLEN! ;) + + int yOffset; + int lives; + + int64_t worldTimer; + int64_t physicsTimer; + int liquidAnimationFrame; + int currentMapHeight; + int spawnTimer; + int spawnIndex; + int spawnSide; + + wsg_t floorTiles[20]; + wsg_t animationTiles[20]; + + lumberjackTile_t tile[400]; + uint8_t anim[400]; + + wsg_t enemySprites[21]; + wsg_t playerSprites[54]; + + wsg_t alertSprite; + + wsg_t slowload[400]; + + lumberjackEntity_t* enemy[8]; + + lumberjackEntity_t* localPlayer; + lumberjackEntity_t* remotePlayer; + + lumberjackGameType_t gameType; + +} lumberjackVars_t; + +#endif \ No newline at end of file diff --git a/main/modes/lumberjack/lumberjackEntity.c b/main/modes/lumberjack/lumberjackEntity.c new file mode 100644 index 000000000..5b0a1f1e1 --- /dev/null +++ b/main/modes/lumberjack/lumberjackEntity.c @@ -0,0 +1,177 @@ +#include +#include "lumberjack_types.h" +#include "lumberjackEntity.h" +#include "lumberjack.h" + +void lumberjackSetupEnemy(lumberjackEntity_t* enemy, int character) +{ + enemy->direction = 1; + enemy->state = LUMBERJACK_RUN; + enemy->maxVX = 0; + enemy->active = false; + enemy->ready = true; + enemy->showAlert = false; + enemy->spriteOffset = 0; + enemy->cW = 15; + enemy->cH = 15; + lumberjackUpdateEnemy(enemy, character); +} + +void lumberjackResetEnemy(lumberjackEntity_t* enemy) +{ + enemy->direction = 1; // This needs to be decided ahead of time + enemy->tileHeight = 1; + if (enemy->state != LUMBERJACK_DEAD) + enemy->state = LUMBERJACK_RUN; + enemy->active = false; + enemy->ready = true; + enemy->y = 0; + enemy->x = 0; + enemy->upgrading = false; + + lumberjackUpdateEnemy(enemy, enemy->type); +} + +void lumberjackRespawnEnemy(lumberjackEntity_t* enemy, int side) +{ + enemy->tileHeight = 1; + enemy->state = LUMBERJACK_RUN; + enemy->active = true; + enemy->ready = false; + enemy->y = 0; + + // I need to figure out why when moving right he appears to move faster + if (side == 1) + { + enemy->direction = 1; // This needs to be decided ahead of time + enemy->x = 0; + enemy->vx = 0; // enemy->maxVX; + enemy->flipped = false; + } + else + { + enemy->direction = -1; // This needs to be decided ahead of time + enemy->x = 279; + enemy->vx = 0; // -enemy->maxVX; + enemy->flipped = true; + } +} + +void lumberjackUpdateEnemy(lumberjackEntity_t* enemy, int newIndex) +{ + enemy->type = newIndex; + + if (enemy->type > enemy->maxLevel) + { + enemy->type = enemy->maxLevel; + } + + enemy->upgrading = false; + + if (newIndex == 0) + { + enemy->width = 15; + enemy->height = 15; + enemy->tileHeight = 1; + enemy->maxVX = 4; + enemy->spriteOffset = 0; + enemy->maxLevel = 2; + + enemy->cW = 15; + enemy->cH = 15; + } + else if (newIndex == 1) + { + enemy->width = 15; + enemy->height = 15; + enemy->tileHeight = 1; + enemy->maxVX = 6; + enemy->spriteOffset = 7; + enemy->maxLevel = 2; + enemy->cW = 15; + enemy->cH = 15; + } + else if (newIndex == 2) + { + enemy->width = 15; + enemy->height = 15; + enemy->tileHeight = 1; + enemy->maxVX = 8; + enemy->spriteOffset = 14; + enemy->maxLevel = 2; + enemy->cW = 15; + enemy->cH = 15; + } +} + +void lumberjackDoEnemyControls(lumberjackEntity_t* enemy) +{ + // pick between types I guess + // if enemy->type 1, 2, or 3... continue + // enemy->direction = -1; +} + +void lumberjackUpdateEnemyCollision(lumberjackEntity_t* enemy) +{ + enemy->cX = enemy->x; + enemy->cY = enemy->y; +} + +void lumberjackUpdatePlayerCollision(lumberjackEntity_t* player) +{ + player->cX = player->x + 2; + player->cW = player->width - 4; + + if (player->state == LUMBERJACK_DUCK) + { + player->cY = player->y + 15; + player->cH = player->height - 14; + } + else + { + player->cY = player->y + 4; + player->cH = player->height - 8; + } +} + +uint8_t lumberjackGetEnemyAnimation(lumberjackEntity_t* enemy) +{ + int animation = enemy->state; + enemy->animationSpeed = 150000; + + if (animation == LUMBERJACK_DEAD) + { + return 5; + } + + if (enemy->upgrading) + { + enemy->animationSpeed /= 2; + } + + if (animation == LUMBERJACK_RUN) + { + const int anim[] = {0, 1, 2, 3}; + return anim[enemy->currentFrame % 4]; + } + + if (animation == LUMBERJACK_BUMPED) + { + const int anim[] = {4}; + return anim[enemy->currentFrame % 2]; + } + + if (animation == LUMBERJACK_BUMPED_IDLE) + { + const int anim[] = {5, 6}; + return anim[enemy->currentFrame % 2]; + } + + return 0; +} + +bool checkCollision(lumberjackEntity_t* AA, lumberjackEntity_t* BB) +{ + return (AA->x < BB->x + BB->width && AA->x + AA->width > BB->x && AA->y < BB->y + BB->height + && AA->y + AA->height > BB->y); +} diff --git a/main/modes/lumberjack/lumberjackEntity.h b/main/modes/lumberjack/lumberjackEntity.h new file mode 100644 index 000000000..ced2eabaf --- /dev/null +++ b/main/modes/lumberjack/lumberjackEntity.h @@ -0,0 +1,66 @@ +#ifndef _LUMBERJACK_ENEMY_H_ +#define _LUMBERJACK_ENEMY_H_ + +#include "swadge2024.h" + +typedef struct +{ + bool flipped; + bool onGround; + bool active; + + bool flying; + + bool attackPressed; + bool attackThisFrame; + + bool jumping; + bool jumpPressed; + bool jumpReady; + int jumpTimer; + + int state; + int currentFrame; + int drawFrame; + int x; + int y; + int spriteOffset; + int vx; + float vy; + int maxVX; + int type; + int maxLevel; + int respawn; + + int cX; + int cY; + int8_t cW; + int8_t cH; + + bool upgrading; + bool ready; // Ready to be placed because it's not in game + bool showAlert; + + int width; + int height; + int tileHeight; + + int direction; + int animationSpeed; + int64_t timerFrameUpdate; + char name[16]; + +} lumberjackEntity_t; + +void lumberjackSetupEnemy(lumberjackEntity_t* enemy, int character); +uint8_t lumberjackGetEnemyAnimation(lumberjackEntity_t* enemy); +void lumberjackResetEnemy(lumberjackEntity_t* enemy); +void lumberjackRespawnEnemy(lumberjackEntity_t* enemy, int side); +bool checkCollision(lumberjackEntity_t* AA, lumberjackEntity_t* BB); +void lumberjackUpdateEnemy(lumberjackEntity_t* enemy, int newIndex); +void lumberjackDoEnemyControls(lumberjackEntity_t* enemy); + +void lumberjackUpdateEnemyCollision(lumberjackEntity_t* enemy); +void lumberjackUpdatePlayerCollision(lumberjackEntity_t* player); + +#endif \ No newline at end of file diff --git a/main/modes/lumberjack/lumberjackGame.c b/main/modes/lumberjack/lumberjackGame.c new file mode 100644 index 000000000..48965a529 --- /dev/null +++ b/main/modes/lumberjack/lumberjackGame.c @@ -0,0 +1,1071 @@ +//============================================================================== +// Includes +//============================================================================== + +#include +#include + +#include "swadge2024.h" +#include "lumberjack.h" +#include "lumberjackGame.h" +#include "lumberjackEntity.h" +#include "lumberjackPlayer.h" + +//============================================================================== +// Defines +//============================================================================== + +#define LUMBERJACK_TILEANIMATION_SPEED 150500 + +#define LUMBERJACK_SCREEN_X_OFFSET 299 +#define LUMBERJACK_SCREEN_X_MIN 0 +#define LUMBERJACK_SCREEN_X_MAX 270 + +#define LUMBERJACK_SCREEN_Y_OFFSET 140 +#define LUMBERJACK_SCREEN_Y_MIN -16 +#define LUMBERJACK_SCREEN_Y_MAX 96 + +static lumberjackTile_t* lumberjackGetTile(int x, int y); +static void lumberjackUpdateEntity(lumberjackEntity_t* entity, int64_t elapsedUs); +static bool lumberjackIsCollisionTile(int index); + +void DrawGame(void); + +lumberjackVars_t* lumv; +lumberjackTile_t lumberjackCollisionCheckTiles[32] = {}; + +void lumberjackStartGameMode(lumberjack_t* main, uint8_t characterIndex) +{ + lumv = calloc(1, sizeof(lumberjackVars_t)); + lumv->lumberjackMain = main; + + loadFont("ibm_vga8.font", &lumv->ibm, false); + + bzrStop(); // Stop the buzzer? + + lumv->worldTimer = 0; + lumv->liquidAnimationFrame = 0; + lumv->loaded = false; + lumv->gameType = main->gameMode; + + ESP_LOGI(LUM_TAG, "Loading floor Tiles"); + loadWsg("bottom_floor1.wsg", &lumv->floorTiles[0], true); + loadWsg("bottom_floor2.wsg", &lumv->floorTiles[1], true); + loadWsg("bottom_floor3.wsg", &lumv->floorTiles[2], true); + loadWsg("bottom_floor4.wsg", &lumv->floorTiles[3], true); + loadWsg("bottom_floor5.wsg", &lumv->floorTiles[4], true); + loadWsg("bottom_floor6.wsg", &lumv->floorTiles[5], true); + loadWsg("bottom_floor7.wsg", &lumv->floorTiles[6], true); + loadWsg("bottom_floor8.wsg", &lumv->floorTiles[7], true); + loadWsg("bottom_floor9.wsg", &lumv->floorTiles[8], true); + loadWsg("bottom_floor10.wsg", &lumv->floorTiles[9], true); + ESP_LOGI(LUM_TAG, "Loading Animation Tiles"); + + loadWsg("water_floor1.wsg", &lumv->animationTiles[0], true); + loadWsg("water_floor2.wsg", &lumv->animationTiles[1], true); + loadWsg("water_floor3.wsg", &lumv->animationTiles[2], true); + loadWsg("water_floor4.wsg", &lumv->animationTiles[3], true); + loadWsg("water_floor0.wsg", &lumv->animationTiles[4], true); + loadWsg("water_floor0.wsg", &lumv->animationTiles[5], true); + loadWsg("water_floor0.wsg", &lumv->animationTiles[6], true); + loadWsg("water_floor0.wsg", &lumv->animationTiles[7], true); + loadWsg("water_floor_b1.wsg", &lumv->animationTiles[8], true); + loadWsg("water_floor_b2.wsg", &lumv->animationTiles[9], true); + loadWsg("water_floor_b3.wsg", &lumv->animationTiles[10], true); + loadWsg("water_floor_b4.wsg", &lumv->animationTiles[11], true); + + ESP_LOGI(LUM_TAG, "Loading Characters"); + loadWsg("lumbers_red_1.wsg", &lumv->playerSprites[0], true); + loadWsg("lumbers_red_2.wsg", &lumv->playerSprites[1], true); + loadWsg("lumbers_red_3.wsg", &lumv->playerSprites[2], true); + loadWsg("lumbers_red_4.wsg", &lumv->playerSprites[3], + true); // These two things break 3 seconds after the game loads + loadWsg("lumbers_red_5.wsg", &lumv->playerSprites[4], true); // I think the memory is being replaces + loadWsg("lumbers_red_6.wsg", &lumv->playerSprites[5], true); + loadWsg("lumbers_red_7.wsg", &lumv->playerSprites[6], true); + loadWsg("lumbers_red_8.wsg", &lumv->playerSprites[7], true); + loadWsg("lumbers_red_9.wsg", &lumv->playerSprites[8], true); + loadWsg("lumbers_red_10.wsg", &lumv->playerSprites[9], true); + loadWsg("lumbers_red_11.wsg", &lumv->playerSprites[10], true); + loadWsg("lumbers_red_12.wsg", &lumv->playerSprites[11], true); + loadWsg("lumbers_red_13.wsg", &lumv->playerSprites[12], true); + loadWsg("lumbers_red_14.wsg", &lumv->playerSprites[13], true); + loadWsg("lumbers_red_15.wsg", &lumv->playerSprites[14], true); + loadWsg("lumbers_red_16.wsg", &lumv->playerSprites[15], true); + loadWsg("lumbers_red_17.wsg", &lumv->playerSprites[16], true); + + loadWsg("lumbers_green_1.wsg", &lumv->playerSprites[17], true); + loadWsg("lumbers_green_2.wsg", &lumv->playerSprites[18], true); + loadWsg("lumbers_green_3.wsg", &lumv->playerSprites[19], true); + loadWsg("lumbers_green_4.wsg", &lumv->playerSprites[20], true); + loadWsg("lumbers_green_5.wsg", &lumv->playerSprites[21], true); + loadWsg("lumbers_green_6.wsg", &lumv->playerSprites[22], true); + loadWsg("lumbers_green_7.wsg", &lumv->playerSprites[23], true); + loadWsg("lumbers_green_8.wsg", &lumv->playerSprites[24], true); + loadWsg("lumbers_green_9.wsg", &lumv->playerSprites[25], true); + loadWsg("lumbers_green_10.wsg", &lumv->playerSprites[26], true); + loadWsg("lumbers_green_11.wsg", &lumv->playerSprites[27], true); + loadWsg("lumbers_green_12.wsg", &lumv->playerSprites[28], true); + loadWsg("lumbers_green_13.wsg", &lumv->playerSprites[29], true); + loadWsg("lumbers_green_14.wsg", &lumv->playerSprites[30], true); + loadWsg("lumbers_green_15.wsg", &lumv->playerSprites[31], true); + loadWsg("lumbers_green_16.wsg", &lumv->playerSprites[32], true); + loadWsg("lumbers_green_17.wsg", &lumv->playerSprites[33], true); + + loadWsg("secret_swadgeland_1.wsg", &lumv->playerSprites[34], true); + loadWsg("secret_swadgeland_2.wsg", &lumv->playerSprites[35], true); + loadWsg("secret_swadgeland_3.wsg", &lumv->playerSprites[36], true); + loadWsg("secret_swadgeland_4.wsg", &lumv->playerSprites[37], true); + loadWsg("secret_swadgeland_5.wsg", &lumv->playerSprites[38], true); + loadWsg("secret_swadgeland_6.wsg", &lumv->playerSprites[39], true); + loadWsg("secret_swadgeland_7.wsg", &lumv->playerSprites[40], true); + loadWsg("secret_swadgeland_8.wsg", &lumv->playerSprites[41], true); + loadWsg("secret_swadgeland_9.wsg", &lumv->playerSprites[42], true); + loadWsg("secret_swadgeland_10.wsg", &lumv->playerSprites[43], true); + loadWsg("secret_swadgeland_11.wsg", &lumv->playerSprites[44], true); + loadWsg("secret_swadgeland_12.wsg", &lumv->playerSprites[45], true); + loadWsg("secret_swadgeland_13.wsg", &lumv->playerSprites[46], true); + loadWsg("secret_swadgeland_14.wsg", &lumv->playerSprites[47], true); + loadWsg("secret_swadgeland_15.wsg", &lumv->playerSprites[48], true); + loadWsg("secret_swadgeland_16.wsg", &lumv->playerSprites[49], true); + loadWsg("secret_swadgeland_17.wsg", &lumv->playerSprites[50], true); + loadWsg("secret_swadgeland_18.wsg", &lumv->playerSprites[51], true); + loadWsg("secret_swadgeland_19.wsg", &lumv->playerSprites[52], true); + loadWsg("secret_swadgeland_20.wsg", &lumv->playerSprites[53], true); + loadWsg("secret_swadgeland_21.wsg", &lumv->playerSprites[54], true); + + ESP_LOGI(LUM_TAG, "Loading Enemies"); + loadWsg("enemy_a1.wsg", &lumv->enemySprites[0], true); + loadWsg("enemy_a2.wsg", &lumv->enemySprites[1], true); + loadWsg("enemy_a3.wsg", &lumv->enemySprites[2], true); + loadWsg("enemy_a4.wsg", &lumv->enemySprites[3], true); + loadWsg("enemy_a5.wsg", &lumv->enemySprites[4], true); + loadWsg("enemy_a6.wsg", &lumv->enemySprites[5], true); + loadWsg("enemy_a7.wsg", &lumv->enemySprites[6], true); + loadWsg("enemy_b1.wsg", &lumv->enemySprites[7], true); + loadWsg("enemy_b2.wsg", &lumv->enemySprites[8], true); + loadWsg("enemy_b3.wsg", &lumv->enemySprites[9], true); + loadWsg("enemy_b4.wsg", &lumv->enemySprites[10], true); + loadWsg("enemy_b5.wsg", &lumv->enemySprites[11], true); + loadWsg("enemy_b6.wsg", &lumv->enemySprites[12], true); + loadWsg("enemy_b7.wsg", &lumv->enemySprites[13], true); + loadWsg("enemy_c1.wsg", &lumv->enemySprites[14], true); + loadWsg("enemy_c2.wsg", &lumv->enemySprites[15], true); + loadWsg("enemy_c3.wsg", &lumv->enemySprites[16], true); + loadWsg("enemy_c4.wsg", &lumv->enemySprites[17], true); + loadWsg("enemy_c5.wsg", &lumv->enemySprites[18], true); + loadWsg("enemy_c6.wsg", &lumv->enemySprites[19], true); + loadWsg("enemy_c7.wsg", &lumv->enemySprites[20], true); + + loadWsg("alert.wsg", &lumv->alertSprite, true); + + if (lumv->gameType == LUMBERJACK_MODE_ATTACK) + { + lumberjackSetupLevel(characterIndex); + } + else if (lumv->gameType == LUMBERJACK_MODE_PANIC) + { + lumberjackSetupLevel(characterIndex); + } + + ESP_LOGI(LUM_TAG, "height %d", TFT_HEIGHT); +} + +void lumberjackSetupLevel(int characterIndex) +{ + // This all to be loaded externally + lumv->yOffset = 0; + lumv->currentMapHeight = 21; + lumv->lives = 3; + lumv->spawnIndex = 0; + lumv->spawnTimer = 2750; + lumv->spawnSide = 0; + + lumv->localPlayer = calloc(1, sizeof(lumberjackEntity_t)); + lumv->remotePlayer = calloc(1, sizeof(lumberjackEntity_t)); + lumberjackSetupPlayer(lumv->localPlayer, characterIndex); + lumberjackSpawnPlayer(lumv->localPlayer, 94, 0, 0); + + // snprintf(lumv->localPlayer->name, sizeof(lumv->localPlayer->name), "Player"); + strcpy(lumv->localPlayer->name, " Dennis"); // If you see this... this name means nothing + + for (int eSpawnIndex = 0; eSpawnIndex < 2; eSpawnIndex++) + { + lumv->enemy[eSpawnIndex] = calloc(1, sizeof(lumberjackEntity_t)); + lumberjackSetupEnemy(lumv->enemy[eSpawnIndex], 0); + + sprintf(lumv->enemy[eSpawnIndex]->name, "Enemy %d", eSpawnIndex); + } + + const uint8_t level[] = { + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 4, 4, 4, 4, 4, 4, 4, 0, 0, 0, 0, 4, 4, 4, 4, 4, 4, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 4, 3, 3, 4, 4, 0, + 0, 0, 0, 0, 0, 4, 4, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 4, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 4, 4, 4, 4, 4, 4, 0, 0, 0, 0, 0, 0, 4, 4, 4, 4, 4, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 2, 2, 2, 2, + 2, 2, 2, 2, 2, 2, 5, 0, 0, 0, 0, 0, 6, 7, 8, 8, 8, 8, 8, 8, 8, 8, 8, 9, 10, 0, 0, 0, + }; + + const uint8_t ani[] = { + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 3, 1, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 3, 1, 3, 1, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 2, + }; + + for (int tileIndex = 0; tileIndex < ARRAY_SIZE(ani); tileIndex++) + { + lumv->anim[tileIndex] = ani[tileIndex]; + + lumv->tile[tileIndex].x = tileIndex % 18; + lumv->tile[tileIndex].y = tileIndex / 18; + lumv->tile[tileIndex].type = level[tileIndex]; + } + + ESP_LOGI(LUM_TAG, "LOADED"); +} + +/** + * @brief TODO use this somewhere + */ +void restartLevel(void) +{ + lumberjackRespawn(lumv->localPlayer); +} + +void lumberjackGameLoop(int64_t elapsedUs) +{ + baseMode(elapsedUs); + + // If networked + if (lumv->lumberjackMain->networked && lumv->lumberjackMain->conStatus == CON_ESTABLISHED) + lumberjackUpdateLocation(lumv->localPlayer->x, lumv->localPlayer->y, lumv->localPlayer->drawFrame); + + DrawGame(); +} + +void lumberjackUpdateRemote(int remoteX, int remoteY, int remoteFrame) +{ + lumv->remotePlayer->x = remoteX; + lumv->remotePlayer->y = remoteY; + lumv->remotePlayer->drawFrame = remoteFrame; + lumv->remotePlayer->active = true; +} + +void baseMode(int64_t elapsedUs) +{ + // Ignore the first frame because everything was loading + // Here we might want to do something like say "On first frame loaded do stuff" + if (lumv->loaded == false) + { + lumv->loaded = true; + + ESP_LOGI(LUM_TAG, "Load Time %ld", (long)elapsedUs); + + // If networked, send "Loaded complete!" ? + + return; + } + + // Update State + buttonEvt_t evt = {0}; + while (checkButtonQueueWrapper(&evt)) + { + lumv->btnState = evt.state; + } + + // return; + + // Check Controls + if (lumv->localPlayer->state != LUMBERJACK_DEAD && lumv->localPlayer->state != LUMBERJACK_UNSPAWNED) + { + bool attackThisFrame = lumv->localPlayer->attackPressed; + lumberjackDoControls(); + + if (!attackThisFrame && lumv->localPlayer->attackPressed) + { + ESP_LOGI(LUM_TAG, "Attack this frame!"); + lumberjackSendAttack(0); + } + } + + for (int enemyIndex = 0; enemyIndex < ARRAY_SIZE(lumv->enemy); enemyIndex++) + { + if (lumv->enemy[enemyIndex] == NULL) + continue; + + lumberjackDoEnemyControls(lumv->enemy[enemyIndex]); + } + + // Clear cruft + lumberjackUpdate(elapsedUs); + + // Check spawn + lumberjackSpawnCheck(elapsedUs); + + for (int colTileIndex = 0; colTileIndex < ARRAY_SIZE(lumberjackCollisionCheckTiles); colTileIndex++) + { + lumberjackCollisionCheckTiles[colTileIndex].type = -1; + lumberjackCollisionCheckTiles[colTileIndex].x = -1; + lumberjackCollisionCheckTiles[colTileIndex].y = -1; + lumberjackCollisionCheckTiles[colTileIndex].collision = -1; + lumberjackCollisionCheckTiles[colTileIndex].index = -1; + lumberjackCollisionCheckTiles[colTileIndex].offset = 0; + lumberjackCollisionCheckTiles[colTileIndex].offset_time = 0; + } + + if (lumv->localPlayer->onGround && !lumv->localPlayer->jumpReady) + { + if (lumv->localPlayer->jumpPressed == false) + { + lumv->localPlayer->jumpReady = true; + } + } + + // Check physics + lumberjackUpdatePlayerCollision(lumv->localPlayer); + + // Enemy + for (int eIdx = 0; eIdx < ARRAY_SIZE(lumv->enemy); eIdx++) + { + lumberjackEntity_t* enemy = lumv->enemy[eIdx]; + if (enemy == NULL || enemy->ready == true) + continue; + + enemy->showAlert = false; + if (enemy->state == LUMBERJACK_BUMPED_IDLE) + { + enemy->respawn -= elapsedUs / 10000; + + enemy->showAlert = enemy->respawn < 200; + if (enemy->showAlert) + { + enemy->upgrading = true; + } + + if (enemy->respawn <= 0) + { + // Hopefully the enemy isn't dead off screen. + enemy->state = LUMBERJACK_RUN; + + enemy->direction = (enemy->flipped) ? -1 : 1; + + enemy->showAlert = false; + enemy->vy = -10; + + lumberjackUpdateEnemy(enemy, enemy->type + 1); + } + } + + lumberjackUpdateEntity(enemy, elapsedUs); + + for (int oeIdx = 0; oeIdx < ARRAY_SIZE(lumv->enemy); oeIdx++) + { + if (lumv->enemy[oeIdx] == NULL) + continue; + + lumberjackUpdateEnemyCollision(lumv->enemy[oeIdx]); + } + } + + // Player + if (lumv->localPlayer->ready) + { + lumv->localPlayer->respawn -= elapsedUs / 10000; + + if (lumv->localPlayer->respawn <= 0 && lumv->lives > 0) + { + // Respawn player + lumv->localPlayer->respawn = 0; + lumberjackRespawn(lumv->localPlayer); + } + } + else if (lumv->localPlayer->state != LUMBERJACK_OFFSCREEN && lumv->localPlayer->state != LUMBERJACK_VICTORY) + { + lumberjackUpdateEntity(lumv->localPlayer, elapsedUs); + + // + for (int enemyIndex = 0; enemyIndex < ARRAY_SIZE(lumv->enemy); enemyIndex++) + { + lumberjackEntity_t* enemy = lumv->enemy[enemyIndex]; + + if (enemy == NULL || lumv->localPlayer->state == LUMBERJACK_DEAD) + continue; + + if (enemy->state != LUMBERJACK_DEAD && enemy->state != LUMBERJACK_OFFSCREEN) + { + // DO AABB checking + if (checkCollision(lumv->localPlayer, enemy)) + { + if (enemy->state == LUMBERJACK_BUMPED || enemy->state == LUMBERJACK_BUMPED_IDLE) + { + enemy->state = LUMBERJACK_DEAD; + enemy->vy = -30; + if (lumv->localPlayer->vx != 0) + { + enemy->direction = abs(lumv->localPlayer->vx) / lumv->localPlayer->vx; + } + else + { + enemy->direction = 0; + } + enemy->vx = enemy->direction * 10; + enemy->active = false; + } + else + { + // Kill player + // ESP_LOGI(LUM_TAG, "KILL PLAYER"); + lumv->localPlayer->state = LUMBERJACK_DEAD; + lumv->localPlayer->vy = -20; + lumv->localPlayer->active = false; + lumv->localPlayer->jumping = false; + lumv->localPlayer->jumpTimer = 0; + } + } + } + } + } + + lumv->worldTimer += elapsedUs; + + if (lumv->worldTimer > LUMBERJACK_TILEANIMATION_SPEED) + { + lumv->worldTimer -= LUMBERJACK_TILEANIMATION_SPEED; + lumv->liquidAnimationFrame++; + lumv->liquidAnimationFrame %= 4; + } + + // Update animation + // Enemy Animation + for (int enemyIndex = 0; enemyIndex < ARRAY_SIZE(lumv->enemy); enemyIndex++) + { + if (lumv->enemy[enemyIndex] == NULL) + continue; + + lumv->enemy[enemyIndex]->timerFrameUpdate += elapsedUs; + if (lumv->enemy[enemyIndex]->timerFrameUpdate > lumv->enemy[enemyIndex]->animationSpeed) + { + lumv->enemy[enemyIndex]->currentFrame++; + lumv->enemy[enemyIndex]->timerFrameUpdate = 0; //; + } + } + + // Player + lumv->localPlayer->timerFrameUpdate += elapsedUs; + if (lumv->localPlayer->timerFrameUpdate > lumv->localPlayer->animationSpeed) + { + lumv->localPlayer->currentFrame++; + lumv->localPlayer->timerFrameUpdate = 0; //; + } + + lumv->yOffset = lumv->localPlayer->y - LUMBERJACK_SCREEN_Y_OFFSET; + if (lumv->yOffset < LUMBERJACK_SCREEN_Y_MIN) + lumv->yOffset = LUMBERJACK_SCREEN_Y_MIN; + if (lumv->yOffset > LUMBERJACK_SCREEN_Y_MAX) + lumv->yOffset = LUMBERJACK_SCREEN_Y_MAX; +} + +void DrawGame(void) +{ + // Draw section + // Redraw bottom + lumberjackTileMap(); + + // Draw enemies + + for (int enemyIndex = 0; enemyIndex < ARRAY_SIZE(lumv->enemy); enemyIndex++) + { + if (lumv->enemy[enemyIndex] == NULL || lumv->enemy[enemyIndex]->ready) + continue; + lumberjackEntity_t* enemy = lumv->enemy[enemyIndex]; + + int eFrame = lumberjackGetEnemyAnimation(enemy); + + drawWsg(&lumv->enemySprites[enemy->spriteOffset + eFrame], enemy->x, enemy->y - lumv->yOffset, enemy->flipped, + false, 0); + + if (enemy->x > LUMBERJACK_SCREEN_X_MAX) + { + drawWsg(&lumv->enemySprites[enemy->spriteOffset + eFrame], enemy->x - LUMBERJACK_SCREEN_X_OFFSET, + enemy->y - lumv->yOffset, enemy->flipped, false, 0); + } + + if (enemy->showAlert) + { + // Fix the magic number :( + drawWsg(&lumv->alertSprite, enemy->x + 6, enemy->y - 26 - lumv->yOffset, enemy->flipped, false, 0); + } + } + + int currentFrame = lumberjackGetPlayerAnimation(lumv->localPlayer); + + if (lumv->localPlayer->state == LUMBERJACK_DEAD) + { + // ESP_LOGI(LUM_TAG, "DEAD %d", currentFrame); + } + + lumv->localPlayer->drawFrame = lumv->localPlayer->spriteOffset + currentFrame; + + // This is where it breaks. When it tries to play frame 3 or 4 it crashes. + drawWsg(&lumv->playerSprites[lumv->localPlayer->drawFrame], lumv->localPlayer->x - 4, + lumv->localPlayer->y - lumv->yOffset, lumv->localPlayer->flipped, false, 0); + + if (lumv->localPlayer->x > LUMBERJACK_SCREEN_X_MAX) + { + drawWsg(&lumv->playerSprites[lumv->localPlayer->drawFrame], lumv->localPlayer->x - LUMBERJACK_SCREEN_X_OFFSET, + lumv->localPlayer->y - lumv->yOffset, lumv->localPlayer->flipped, false, 0); + } + + if (lumv->remotePlayer->active) + { + drawWsg(&lumv->playerSprites[lumv->remotePlayer->drawFrame], lumv->remotePlayer->x - 4, + lumv->remotePlayer->y - lumv->yOffset, false, false, 0); + } + + // Debug + + char debug[20] = {0}; + snprintf(debug, sizeof(debug), "Debug: %d %d %d", lumv->localPlayer->x, lumv->localPlayer->y, + lumv->localPlayer->cH); + + drawText(&lumv->ibm, c000, debug, 16, 16); + + // drawRect(lumv->localPlayer->cX, lumv->localPlayer->cY - lumv->yOffset, lumv->localPlayer->cX + + // lumv->localPlayer->cW, lumv->localPlayer->cY - lumv->yOffset + lumv->localPlayer->cH, c050); + + if (lumv->localPlayer->jumpPressed) + { + drawText(&lumv->ibm, c555, "A", 16, 32); + } + else + { + drawText(&lumv->ibm, c000, "A", 16, 32); + } + + if (lumv->localPlayer->attackPressed) + { + drawText(&lumv->ibm, c555, "B", 48, 32); + } + else + { + drawText(&lumv->ibm, c000, "B", 48, 32); + } +} + +void lumberjackSpawnCheck(int64_t elapseUs) +{ + if (lumv->spawnTimer >= 0) + { + bool spawnReady = true; + + float elapse = (elapseUs / 1000); + lumv->spawnTimer -= elapse; + + if (lumv->spawnTimer < 0) + { + for (int enemyIndex = 0; enemyIndex < ARRAY_SIZE(lumv->enemy); enemyIndex++) + { + if (lumv->enemy[enemyIndex] == NULL) + continue; + + if (lumv->enemy[enemyIndex]->ready && lumv->enemy[enemyIndex]->state != LUMBERJACK_DEAD) + { + lumv->spawnSide++; + lumv->spawnSide %= 2; + + lumv->spawnTimer = 750; + + lumberjackRespawnEnemy(lumv->enemy[enemyIndex], lumv->spawnSide); + spawnReady = false; + // break; + } + } + + if (spawnReady) + { + // No one was spawned. + lumv->spawnTimer = 500; + } + } + } +} + +static void lumberjackUpdateEntity(lumberjackEntity_t* entity, int64_t elapsedUs) +{ + bool onGround = false; + + // World wrap + entity->x %= 295; + if (entity->x < -20) + { + entity->x += 295; + } + + if (entity->state == LUMBERJACK_BUMPED) + { + entity->vy -= 2; + entity->respawn = 1500; + + if (entity->vy >= 0) + { + entity->state = LUMBERJACK_BUMPED_IDLE; + } + } + + // Check jumping first + if (entity->jumpPressed && entity->active) + { + if (entity->onGround && entity->jumpReady) + { + // Check if player CAN jump + entity->jumpReady = false; + entity->jumping = true; + entity->vy = -15; + entity->jumpTimer = 225000; + entity->onGround = false; + } + else if (entity->jumping) + { + entity->vy -= 6; + } + } + + if (entity->jumpTimer > 0 && entity->active) + { + entity->jumpTimer -= elapsedUs; + if (entity->jumpTimer <= 0) + { + entity->jumpTimer = 0; + entity->jumping = false; + } + } + + if (entity->jumping == false && entity->flying == false) + entity->vy += 6; // Fix gravity + + if (entity->active) + { + if (entity->direction > 0 && entity->state != LUMBERJACK_DUCK) + { + if (entity->onGround) + entity->vx += 5; + else + entity->vx += 8; + } + else if (entity->direction < 0 && entity->state != LUMBERJACK_DUCK) + { + if (entity->onGround) + entity->vx -= 5; + else + entity->vx -= 8; + } + else + { + if (entity->onGround) + entity->vx *= .1; + } + } + + if (entity->vx > entity->maxVX) + entity->vx = entity->maxVX; + if (entity->vx < -entity->maxVX) + entity->vx = -entity->maxVX; + if (entity->vy < -30) + entity->vy = -30; + if (entity->vy > 16) + entity->vy = 16; + + float elapsed = elapsedUs / 100000.0; + int destinationX = entity->x + (int)(entity->vx * elapsed); + int destinationY = entity->y + (entity->vy * elapsed); + + if (entity->vx < 0 && entity->active) + { + lumberjackTile_t* tileA = lumberjackGetTile(destinationX + 0, entity->y + 2); + lumberjackTile_t* tileB = lumberjackGetTile(destinationX + 0, entity->y + entity->height); + + if ((tileA != NULL && lumberjackIsCollisionTile(tileA->type)) + || (tileB != NULL && lumberjackIsCollisionTile(tileB->type))) + { + destinationX = entity->x; + entity->vx = 0; + } + } + else if (entity->vx > 0 && entity->active) + { + lumberjackTile_t* tileA = lumberjackGetTile(destinationX + 24, entity->y + 2); + lumberjackTile_t* tileB = lumberjackGetTile(destinationX + 24, entity->y + entity->height); + + if ((tileA != NULL && lumberjackIsCollisionTile(tileA->type)) + || (tileB != NULL && lumberjackIsCollisionTile(tileB->type))) + { + destinationX = entity->x; + entity->vx = 0; + } + } + + if (entity->vy < 0 && entity->active) + { + lumberjackTile_t* tileA = lumberjackGetTile(destinationX, destinationY); + lumberjackTile_t* tileB = lumberjackGetTile(destinationX + 16, destinationY); + + if (lumberjackIsCollisionTile(tileA->type) || lumberjackIsCollisionTile(tileB->type)) + { + destinationY = ((tileA->y + 1) * 16); + entity->jumpTimer = 0; + entity->jumping = false; + entity->vy = 0; + + if (lumberjackIsCollisionTile(tileA->type)) + { + lumv->tile[tileA->index].offset = 10; + lumv->tile[tileA->index].offset_time = 100; + + lumberjackDetectBump(tileA); + } + + if (lumberjackIsCollisionTile(tileB->type)) + { + lumberjackDetectBump(tileB); + + lumv->tile[tileB->index].offset = 10; + lumv->tile[tileB->index].offset_time = 100; + } + } + } + else if (entity->vy > 0 && entity->active) + { + lumberjackTile_t* tileA = lumberjackGetTile(destinationX, destinationY + entity->height); + lumberjackTile_t* tileB = lumberjackGetTile(destinationX + 16, destinationY + entity->height); + + if ((tileA != NULL && lumberjackIsCollisionTile(tileA->type)) + || (tileB != NULL && lumberjackIsCollisionTile(tileB->type))) + { + destinationY = ((tileA->y - entity->tileHeight) * 16); + entity->vy = 0; + onGround = true; + } + } + + entity->onGround = onGround; + + if (entity->vx > entity->maxVX) + { + // ESP_LOGI(LUM_TAG, "ERROR"); + } + + entity->x = destinationX; + entity->y = destinationY; + + if (entity->y > 350) + { + entity->y = 350; + entity->active = false; + if (entity->state == LUMBERJACK_DEAD) + { + // + if (entity == lumv->localPlayer) + { + if (entity->respawn == 0) + { + // ESP_LOGI(LUM_TAG, "DEAD & hit the ground %d", entity->respawn); + entity->respawn = 250; + entity->ready = true; + return; + } + } + else + { + // Entity is not local player + } + } + + if (entity->state != LUMBERJACK_OFFSCREEN && entity->state != LUMBERJACK_DEAD) + { + if (entity == lumv->localPlayer) + { + entity->respawn = 500; + entity->ready = true; + lumv->localPlayer->state = LUMBERJACK_DEAD; + return; + } + else + { + entity->active = false; + entity->ready = true; + } + } + if (entity->state != LUMBERJACK_DEAD) + entity->state = LUMBERJACK_OFFSCREEN; + } +} + +void lumberjackUpdate(int64_t elapseUs) +{ + for (int tileIndex = 0; tileIndex < ARRAY_SIZE(lumv->tile); tileIndex++) + { + if (lumv->tile[tileIndex].offset > 0) + { + // ESP_LOGI(LUM_TAG, "Update %d",lumv->tile[tileIndex].offset); + + lumv->tile[tileIndex].offset_time -= elapseUs; + if (lumv->tile[tileIndex].offset_time < 0) + { + lumv->tile[tileIndex].offset_time += 2500; + lumv->tile[tileIndex].offset--; + } + } + } +} + +void lumberjackTileMap(void) +{ + // TODO: Stop drawing any area of the map that is off screen + for (int y = 0; y < 21; y++) + { + for (int x = 0; x < 18; x++) + { + int index = (y * 18) + x; + int tileIndex = lumv->tile[index].type; + int animIndex = lumv->anim[index]; + int offset = lumv->tile[index].offset; + + if ((y * 16) - lumv->yOffset >= -16) + { + if (animIndex > 0 && animIndex < 4) + { + drawWsgSimple(&lumv->animationTiles[((animIndex - 1) * 4) + (lumv->liquidAnimationFrame % 4)], + x * 16, (y * 16) - lumv->yOffset + 8 - offset); + } + + if (tileIndex > 0 && tileIndex < 13) + { + if (tileIndex < 11) + { + drawWsgSimple(&lumv->floorTiles[tileIndex - 1], x * 16, (y * 16) - lumv->yOffset - offset); + } + } + } + } + } +} + +void lumberjackDoControls(void) +{ + lumv->localPlayer->cW = 15; + int previousState = lumv->localPlayer->state; + bool buttonPressed = false; + if (lumv->btnState & PB_LEFT) + { + lumv->localPlayer->flipped = true; + lumv->localPlayer->state = 2; + lumv->localPlayer->direction = -1; + buttonPressed = true; + } + else if (lumv->btnState & PB_RIGHT) + { + lumv->localPlayer->flipped = false; + lumv->localPlayer->state = 2; + lumv->localPlayer->direction = 1; + + buttonPressed = true; + } + else + { + lumv->localPlayer->direction = 0; + } + + if (lumv->btnState & PB_DOWN) + { + buttonPressed = true; + if (lumv->localPlayer->onGround) + { + lumv->localPlayer->state = LUMBERJACK_DUCK; + + lumv->localPlayer->cW = 15; + lumv->localPlayer->cH = 15; + } + } + else if (lumv->btnState & PB_UP) + { + buttonPressed = true; + } + + // TODO: This is sloppy Troy + if (lumv->btnState & PB_A) + { + lumv->localPlayer->jumpPressed = true; + } + else + { + lumv->localPlayer->jumpPressed = false; + } + + // TODO: This is sloppy too Troy + if (lumv->btnState & PB_B) + { + lumv->localPlayer->attackPressed = true; + } + else + { + lumv->localPlayer->attackPressed = false; + } + + if (buttonPressed == false && lumv->localPlayer->active) + { + lumv->localPlayer->state = 1; // Do a ton of other checks here + } + + if (lumv->localPlayer->state != previousState) + { + // lumv->localPlayer->currentFrame = 0; + lumv->localPlayer->timerFrameUpdate = 0; + } +} + +static lumberjackTile_t* lumberjackGetTile(int x, int y) +{ + int tx = (int)x / 16; + int ty = (int)y / 16; + + if (tx < 0) + tx = 17; + if (tx > 17) + tx = 0; + + if (ty < 0) + ty = 0; + if (ty > lumv->currentMapHeight) + ty = lumv->currentMapHeight; + + // int test = -1; + for (int colTileIndex = 0; colTileIndex < 32; colTileIndex++) + { + if (lumberjackCollisionCheckTiles[colTileIndex].type == -1) + { + if (lumberjackCollisionCheckTiles[colTileIndex].index == (ty * 18) + tx) + return &lumberjackCollisionCheckTiles[colTileIndex]; + + lumberjackCollisionCheckTiles[colTileIndex].index = (ty * 18) + tx; + lumberjackCollisionCheckTiles[colTileIndex].x = tx; + lumberjackCollisionCheckTiles[colTileIndex].y = ty; + lumberjackCollisionCheckTiles[colTileIndex].type = lumv->tile[(ty * 18) + tx].type; + + return &lumberjackCollisionCheckTiles[colTileIndex]; + } + // test = i; + } + + // ESP_LOGI(LUM_TAG,"NO TILE at %d %d!", test, ty); + return NULL; +} + +static bool lumberjackIsCollisionTile(int index) +{ + if (index == 0 || index == 1 || index == 6 || index == 5 || index == 10) + return false; + + return true; +} + +void lumberjackDetectBump(lumberjackTile_t* tile) +{ + if (lumv->localPlayer->state != LUMBERJACK_BUMPED && lumv->localPlayer->state != LUMBERJACK_DEAD) + { + // TODO put in bump the player + } + + for (int enemyIndex = 0; enemyIndex < ARRAY_SIZE(lumv->enemy); enemyIndex++) + { + lumberjackEntity_t* enemy = lumv->enemy[enemyIndex]; + if (enemy == NULL) + continue; + + if (enemy->onGround || enemy->flying) + { + int tx = ((enemy->x - 8) / 16); + int ty = ((enemy->y) / 16) + 1; + int tx2 = ((enemy->x + 8) / 16); + + if (tx < 0) + tx = 0; + if (tx > 17) + tx = 17; + if (tx2 < 0) + tx2 = 0; + if (tx2 > 17) + tx2 = 17; + if (ty < 0) + ty = 0; + if (ty > lumv->currentMapHeight) + ty = lumv->currentMapHeight; + + lumberjackTile_t* tileA = &lumv->tile[(ty * 18) + tx]; + lumberjackTile_t* tileB = &lumv->tile[(ty * 18) + tx2]; + + if ((tileA != NULL && (ty * 18) + tx == tile->index) || (tileB != NULL && (ty * 18) + tx2 == tile->index)) + { + enemy->vy = -20; + enemy->onGround = false; + + if (enemy->state == LUMBERJACK_BUMPED_IDLE) + { + enemy->state = LUMBERJACK_RUN; + enemy->direction = enemy->flipped ? -1 : 1; + } + else + { + enemy->direction = 0; + enemy->state = LUMBERJACK_BUMPED; + } + } + } + } +} + +/// +/// + +void lumberjackExitGameMode(void) +{ + // Everything crashes if you don't load it first + if (lumv == NULL) + return; + + //** FREE THE SPRITES **// + // Free the enemies + for (int i = 0; i < ARRAY_SIZE(lumv->enemySprites); i++) + { + freeWsg(&lumv->enemySprites[i]); + } + + // Free the players + for (int i = 0; i < ARRAY_SIZE(lumv->playerSprites); i++) + { + freeWsg(&lumv->playerSprites[i]); + } + + // Free the tiles + for (int i = 0; i < ARRAY_SIZE(lumv->animationTiles); i++) + { + freeWsg(&lumv->animationTiles[i]); + } + + freeWsg(&lumv->alertSprite); + + freeFont(&lumv->ibm); + free(lumv); +} diff --git a/main/modes/lumberjack/lumberjackGame.h b/main/modes/lumberjack/lumberjackGame.h new file mode 100644 index 000000000..f5646a930 --- /dev/null +++ b/main/modes/lumberjack/lumberjackGame.h @@ -0,0 +1,27 @@ +#ifndef _LUMBERJACK_MODE_H + #define _LUMBERJACK_MODE_H_ + +void lumberjackStartGameMode(lumberjack_t* main, uint8_t characterIndex); + +// void lumberjackStartGameMode(lumberjackGameType_t type, uint8_t characterIndex); +void lumberjackExitGameMode(void); +void lumberjackSetupLevel(int index); +void lumberjackDoControls(void); +void lumberjackTileMap(void); +void lumberjackUpdate(int64_t elapseUs); + +void lumberjackGameLoop(int64_t elapsedUs); +void lumberjackUpdateLocation(int ghostX, int ghostY, int frame); +void lumberjackUpdateRemote(int remoteX, int remoteY, int remoteFrame); + +void restartLevel(void); + +void lumberjackGameDebugLoop(int64_t elapsedUs); + +void lumberjackDetectBump(lumberjackTile_t* tile); +void lumberjackSpawnCheck(int64_t elapseUs); + +void baseMode(int64_t elapsedUs); +void lumberjackSendAttack(int number); + +#endif \ No newline at end of file diff --git a/main/modes/lumberjack/lumberjackPlayer.c b/main/modes/lumberjack/lumberjackPlayer.c new file mode 100644 index 000000000..08b90b529 --- /dev/null +++ b/main/modes/lumberjack/lumberjackPlayer.c @@ -0,0 +1,131 @@ + +#include "lumberjack_types.h" +#include "lumberjackEntity.h" +#include "lumberjackPlayer.h" + +#include + +#define LUMBERJACK_DEFAULT_ANIMATION_SPEED 150000 +#define LUMBERJACK_SPAWN_Y 270 +#define LUMBERJACK_HERO_WIDTH 24 +#define LUMBERJACK_HERO_HEIGHT 31 +#define LUMBERJACK_HERO_DUCK_HEIGHT 31 + +void lumberjackSetupPlayer(lumberjackEntity_t* hero, int character) +{ + hero->height = LUMBERJACK_HERO_HEIGHT; + hero->width = LUMBERJACK_HERO_WIDTH; + hero->tileHeight = 2; + hero->maxVX = 15; + hero->active = true; + hero->showAlert = false; + hero->upgrading = false; + hero->spriteOffset = 0; + hero->maxLevel = character; + hero->type = character; + hero->state = LUMBERJACK_UNSPAWNED; + + if (character == 0) + { + hero->spriteOffset = 0; + } + else if (character == 1) + { + hero->spriteOffset = 17; + } + else + { + hero->spriteOffset = 34; + } +} + +void lumberjackSpawnPlayer(lumberjackEntity_t* hero, int x, int y, int facing) +{ + hero->x = x; + hero->y = LUMBERJACK_SPAWN_Y; + hero->vx = 0; + hero->maxVX = 15; + hero->vy = 0; + hero->flipped = (facing == 0); + hero->state = LUMBERJACK_IDLE; + hero->timerFrameUpdate = 0; + hero->active = true; + hero->onGround = true; + hero->ready = false; + hero->respawn = 0; +} + +void lumberjackRespawn(lumberjackEntity_t* hero) +{ + hero->x = 130; + hero->maxVX = 15; + hero->y = LUMBERJACK_SPAWN_Y; + hero->active = true; + hero->ready = false; + hero->vx = 0; + hero->vy = 0; + hero->flipped = 0; + hero->state = LUMBERJACK_IDLE; + hero->timerFrameUpdate = 0; + hero->onGround = true; + hero->maxLevel = 0; +} + +int lumberjackGetPlayerAnimation(lumberjackEntity_t* hero) +{ + // int animationNone[] = {0}; + + int animation = hero->state; + hero->animationSpeed = LUMBERJACK_DEFAULT_ANIMATION_SPEED; + hero->height = LUMBERJACK_HERO_HEIGHT; + + if (hero->onGround == false && hero->jumping == false && hero->active) + { + const int animationFall[] = {13}; + return animationFall[hero->currentFrame % ARRAY_SIZE(animationFall)]; + } + + if (animation == LUMBERJACK_DUCK) + { + const int animationDuck[] = {16}; + hero->height = LUMBERJACK_HERO_DUCK_HEIGHT; + hero->animationSpeed = LUMBERJACK_DEFAULT_ANIMATION_SPEED; + return animationDuck[hero->currentFrame % ARRAY_SIZE(animationDuck)]; + } + + if (animation == LUMBERJACK_RUN) + { + const int animationRun[] = {7, 8, 9, 10, 11, 12}; + hero->animationSpeed = 90000; + return animationRun[hero->currentFrame % ARRAY_SIZE(animationRun)]; + } + + if (animation == LUMBERJACK_IDLE) + { + const int animationIdle[] = {0, 1, 2, 1, 0, 1, 2, 1, 0, 1, 2, 1, 0, 1, 3, 1}; + hero->animationSpeed = LUMBERJACK_DEFAULT_ANIMATION_SPEED; + return animationIdle[hero->currentFrame % ARRAY_SIZE(animationIdle)]; + } + + if (animation == LUMBERJACK_DEAD) + { + const int animationDead[] = {14}; + hero->animationSpeed = LUMBERJACK_DEFAULT_ANIMATION_SPEED; + return animationDead[hero->currentFrame % ARRAY_SIZE(animationDead)]; + } + + if (animation == LUMBERJACK_VICTORY) + { + const int animationVictory[] = {15}; + hero->animationSpeed = LUMBERJACK_DEFAULT_ANIMATION_SPEED; + return animationVictory[hero->currentFrame % ARRAY_SIZE(animationVictory)]; + } + if (animation == LUMBERJACK_CLIMB) + { + const int animationClimb[] = {17, 18, 19, 20}; + hero->animationSpeed = LUMBERJACK_DEFAULT_ANIMATION_SPEED; + return animationClimb[hero->currentFrame % ARRAY_SIZE(animationClimb)]; + } + + return 0; +} \ No newline at end of file diff --git a/main/modes/lumberjack/lumberjackPlayer.h b/main/modes/lumberjack/lumberjackPlayer.h new file mode 100644 index 000000000..dff11ff6c --- /dev/null +++ b/main/modes/lumberjack/lumberjackPlayer.h @@ -0,0 +1,11 @@ +#ifndef _LUMBERJACK_PLAYER_H_ +#define _LUMBERJACK_PLAYER_H_ + +#include "lumberjack_types.h" + +void lumberjackSpawnPlayer(lumberjackEntity_t* hero, int x, int y, int facing); +void lumberjackRespawn(lumberjackEntity_t* hero); +int lumberjackGetPlayerAnimation(lumberjackEntity_t* hero); +void lumberjackSetupPlayer(lumberjackEntity_t* hero, int character); + +#endif \ No newline at end of file diff --git a/main/modes/lumberjack/lumberjack_types.h b/main/modes/lumberjack/lumberjack_types.h new file mode 100644 index 000000000..03453915e --- /dev/null +++ b/main/modes/lumberjack/lumberjack_types.h @@ -0,0 +1,23 @@ +#ifndef _MODE_LUMBERJACK_TYPES_H_ +#define _MODE_LUMBERJACK_TYPES_H_ + +enum lumberjackPlayerState +{ + LUMBERJACK_DEAD = -1, + LUMBERJACK_UNSPAWNED = 0, + LUMBERJACK_IDLE = 1, + LUMBERJACK_RUN = 2, + LUMBERJACK_DUCK = 3, + LUMBERJACK_VICTORY = 4, + LUMBERJACK_CLIMB = 5, + LUMBERJACK_FALLING = 6, + LUMBERJACK_OFFSCREEN = 7, // reserved for enemies only + LUMBERJACK_BUMPED = 8, + LUMBERJACK_BUMPED_IDLE = 9 +}; + +// Animation speeds +// 90000 - run +// 150000 - idle + +#endif \ No newline at end of file diff --git a/main/modes/mainMenu/mainMenu.c b/main/modes/mainMenu/mainMenu.c index 1f4b2ed0c..6d8fcdd2c 100644 --- a/main/modes/mainMenu/mainMenu.c +++ b/main/modes/mainMenu/mainMenu.c @@ -5,13 +5,24 @@ #include "swadge2024.h" #include "mainMenu.h" + +#include "accelTest.h" +#include "breakout.h" +#include "colorchord.h" +#include "dance.h" #include "demoMode.h" +#include "gamepad.h" #include "jukebox.h" +#include "lumberjack.h" +#include "marbles.h" +#include "mode_paint.h" +#include "mode_ray.h" +#include "paint_share.h" #include "pong.h" -#include "colorchord.h" -#include "dance.h" -#include "tunernome.h" +#include "pushy.h" +#include "soko.h" #include "touchTest.h" +#include "tunernome.h" #include "settingsManager.h" @@ -106,13 +117,33 @@ static void mainMenuEnterMode(void) mainMenu->menu = initMenu(mainMenuName, mainMenuCb); // Add single items - addSingleItemToMenu(mainMenu->menu, demoMode.modeName); + mainMenu->menu = startSubMenu(mainMenu->menu, "Games"); + addSingleItemToMenu(mainMenu->menu, breakoutMode.modeName); + addSingleItemToMenu(mainMenu->menu, lumberjackMode.modeName); + addSingleItemToMenu(mainMenu->menu, marblesMode.modeName); addSingleItemToMenu(mainMenu->menu, pongMode.modeName); + addSingleItemToMenu(mainMenu->menu, pushyMode.modeName); + addSingleItemToMenu(mainMenu->menu, rayMode.modeName); + addSingleItemToMenu(mainMenu->menu, sokoMode.modeName); + mainMenu->menu = endSubMenu(mainMenu->menu); + + mainMenu->menu = startSubMenu(mainMenu->menu, "Music"); addSingleItemToMenu(mainMenu->menu, colorchordMode.modeName); - addSingleItemToMenu(mainMenu->menu, danceMode.modeName); - addSingleItemToMenu(mainMenu->menu, tunernomeMode.modeName); addSingleItemToMenu(mainMenu->menu, jukeboxMode.modeName); + addSingleItemToMenu(mainMenu->menu, tunernomeMode.modeName); + mainMenu->menu = endSubMenu(mainMenu->menu); + + mainMenu->menu = startSubMenu(mainMenu->menu, "Utilities"); + addSingleItemToMenu(mainMenu->menu, danceMode.modeName); + addSingleItemToMenu(mainMenu->menu, gamepadMode.modeName); + addSingleItemToMenu(mainMenu->menu, modePaint.modeName); + mainMenu->menu = endSubMenu(mainMenu->menu); + + mainMenu->menu = startSubMenu(mainMenu->menu, "Tests"); + addSingleItemToMenu(mainMenu->menu, accelTestMode.modeName); + addSingleItemToMenu(mainMenu->menu, demoMode.modeName); addSingleItemToMenu(mainMenu->menu, touchTestMode.modeName); + mainMenu->menu = endSubMenu(mainMenu->menu); // Start a submenu for settings mainMenu->menu = startSubMenu(mainMenu->menu, settingsLabel); @@ -188,13 +219,13 @@ static void mainMenuCb(const char* label, bool selected, uint32_t settingVal) if (selected) { // These items enter other modes, so they must be selected - if (label == demoMode.modeName) + if (label == accelTestMode.modeName) { - switchToSwadgeMode(&demoMode); + switchToSwadgeMode(&accelTestMode); } - else if (label == pongMode.modeName) + else if (label == breakoutMode.modeName) { - switchToSwadgeMode(&pongMode); + switchToSwadgeMode(&breakoutMode); } else if (label == colorchordMode.modeName) { @@ -204,18 +235,54 @@ static void mainMenuCb(const char* label, bool selected, uint32_t settingVal) { switchToSwadgeMode(&danceMode); } - else if (label == tunernomeMode.modeName) + else if (label == demoMode.modeName) { - switchToSwadgeMode(&tunernomeMode); + switchToSwadgeMode(&demoMode); + } + else if (label == gamepadMode.modeName) + { + switchToSwadgeMode(&gamepadMode); } else if (label == jukeboxMode.modeName) { switchToSwadgeMode(&jukeboxMode); } + else if (label == lumberjackMode.modeName) + { + switchToSwadgeMode(&lumberjackMode); + } + else if (label == marblesMode.modeName) + { + switchToSwadgeMode(&marblesMode); + } + else if (label == modePaint.modeName) + { + switchToSwadgeMode(&modePaint); + } + else if (label == pongMode.modeName) + { + switchToSwadgeMode(&pongMode); + } + else if (label == pushyMode.modeName) + { + switchToSwadgeMode(&pushyMode); + } + else if (label == rayMode.modeName) + { + switchToSwadgeMode(&rayMode); + } + else if (label == sokoMode.modeName) + { + switchToSwadgeMode(&sokoMode); + } else if (label == touchTestMode.modeName) { switchToSwadgeMode(&touchTestMode); } + else if (label == tunernomeMode.modeName) + { + switchToSwadgeMode(&tunernomeMode); + } } else { diff --git a/main/modes/marbles/marbles.c b/main/modes/marbles/marbles.c new file mode 100644 index 000000000..383f7b4b9 --- /dev/null +++ b/main/modes/marbles/marbles.c @@ -0,0 +1,701 @@ +#include "menu.h" +#include "menuLogbookRenderer.h" +#include "marbles.h" +#include "font.h" +#include "wsg.h" +#include "hdw-tft.h" +#include "linked_list.h" +#include "geometry.h" +#include "trigonometry.h" +#include "esp_random.h" +#include "fill.h" + +#include +#include +#include + +//============================================================================== +// Defines +//============================================================================== + +/// Whether to draw the paths as a debug option +#define DRAW_PATH 1 + +#define MARBLE_R 6 + +//============================================================================== +// Const Variables +//============================================================================== + +static const char marblesName[] = "Marbles"; +static const char marblesPlay[] = "Play"; + +//============================================================================== +// Enums +//============================================================================== + +typedef enum +{ + MAIN_MENU, + IN_LEVEL, +} marblesScreen_t; + +typedef enum +{ + MARBLE, + MARBLE_GROUP, + SHOOTER, +} marblesEntType_t; + +typedef enum +{ + NORMAL, +} marbleType_t; + +//============================================================================== +// Structs +//============================================================================== + +typedef struct +{ + marbleType_t type; + paletteColor_t color; +} marble_t; + +typedef struct +{ + uint16_t x; + uint16_t y; +} point_t; + +/// @brief Contains the path info. Will probably get more complicated later. +typedef struct +{ + uint8_t length; ///< The number of points in the track + point_t* points; ///< The array of points + bool temporary; ///< Whether this track should be deleted with its owner +} marbleTrack_t; + +typedef struct +{ + marblesEntType_t type; ///< Type of this entity + + int16_t x; ///< Entity's X coordinate + int16_t y; ///< Entity's Y coordinate + int16_t velocity; ///< Entity's velocity + int16_t angle; ///< Entity's heading angle, if not on a track + + marbleTrack_t* track; ///< If non-NULL, the entity is traveling along this track + int32_t trackPos; ///< If on a track, this is the entity's position along the track + + union + { + marble_t marble; ///< Entity data for a single marble + + /// @brief Entity data for a touching group of marbles + struct + { + uint8_t count; ///< Number of marbles in this group + marble_t* marbles; ///< Each marble in this group, starting from the tip + } marbleGroup; + + /// @brief Entity data for a marble shooter + struct + { + bool hasMarble; + marble_t nextMarble; + } shooter; + }; +} marblesEntity_t; + +typedef struct +{ + uint16_t index; ///< The index at which the special should appear, replacing the normal marble + marbleType_t type; ///< The type of special marble to generate +} marblesSpecial_t; + +/// @brief Defines the starting state for a level +typedef struct +{ + uint8_t numColors; ///< Number of marble colors to generate + paletteColor_t* colors; ///< Array of marble colors + uint16_t numSpecials; ///< The number of special marbles + marblesSpecial_t* specials; ///< An array of info defining when to generate special marbles + + uint16_t numMarbles; ///< The total number of marbles to generate this level + + marbleTrack_t track; ///< The track that marbles follow + + point_t shooterLoc; ///< The location of the center of the player's marble shooter +} marblesLevel_t; + +/// @brief Holds the state for the entire mode +typedef struct +{ + font_t ibm; + menu_t* menu; + menuLogbookRenderer_t* renderer; + + wsg_t arrow18; + + buttonBit_t buttonState; + int32_t inputRotation; + + marblesScreen_t screen; + marblesLevel_t* level; ///< The current level's initial settings + + list_t entities; ///< The entities in the current level + uint64_t shootCooldown; ///< us remaining until another marble can be shot +} marblesMode_t; + +//============================================================================== +// Function Prototypes +//============================================================================== + +static void marblesEnterMode(void); +static void marblesExitMode(void); +static void marblesMainLoop(int64_t elapsedUs); +static void marblesBackgroundDrawCallback(int16_t x, int16_t y, int16_t w, int16_t h, int16_t up, int16_t upNum); + +static void marblesMenuCb(const char*, bool selected, uint32_t settingVal); + +static void marblesHandleButton(buttonEvt_t evt); +static void marblesUpdatePhysics(int64_t elapsedUs); +static void marblesDrawLevel(void); +static void marblesLoadLevel(void); +static void marblesUnloadLevel(void); + +static bool marblesCalculateTrackPos(uint8_t trackLength, const point_t* points, int32_t position, int16_t* x, + int16_t* y); +static bool collideMarbles(const marblesEntity_t* a, const marblesEntity_t* b); + +//============================================================================== +// Variables +//============================================================================== + +swadgeMode_t marblesMode = { + .modeName = marblesName, + .wifiMode = NO_WIFI, + .overrideUsb = false, + .usesAccelerometer = false, + .usesThermometer = false, + .fnEnterMode = marblesEnterMode, + .fnExitMode = marblesExitMode, + .fnMainLoop = marblesMainLoop, + .fnAudioCallback = NULL, + .fnBackgroundDrawCallback = marblesBackgroundDrawCallback, + .fnEspNowRecvCb = NULL, + .fnEspNowSendCb = NULL, + .fnAdvancedUSB = NULL, +}; + +marblesMode_t* marbles; + +//============================================================================== +// Functions +//============================================================================== + +/** + * This function is called when this mode is started. It should initialize + * variables and start the mode. + */ +static void marblesEnterMode(void) +{ + marbles = calloc(1, sizeof(marblesMode_t)); + loadFont("ibm_vga8.font", &marbles->ibm, false); + + loadWsg("arrow18.wsg", &marbles->arrow18, false); + + marbles->menu = initMenu(marblesName, marblesMenuCb); + addSingleItemToMenu(marbles->menu, marblesPlay); + + marbles->renderer = initMenuLogbookRenderer(&marbles->ibm); + + marbles->screen = MAIN_MENU; +} + +/** + * This function is called when the mode is exited. It should free any allocated memory. + */ +static void marblesExitMode(void) +{ + deinitMenuLogbookRenderer(marbles->renderer); + deinitMenu(marbles->menu); + freeWsg(&marbles->arrow18); + freeFont(&marbles->ibm); + free(marbles); +} + +static void marblesHandleButton(buttonEvt_t evt) +{ + switch (marbles->screen) + { + case MAIN_MENU: + { + marbles->menu = menuButton(marbles->menu, evt); + break; + } + + case IN_LEVEL: + { + switch (evt.button) + { + case PB_UP: + break; + + case PB_DOWN: + break; + + case PB_LEFT: + break; + + case PB_RIGHT: + break; + + case PB_A: + break; + + case PB_START: + marblesUnloadLevel(); + marblesLoadLevel(); + break; + + case PB_SELECT: + break; + + case PB_B: + marbles->screen = MAIN_MENU; + marblesUnloadLevel(); + break; + } + marbles->buttonState = evt.state; + break; + } + } +} + +/** + * @brief Update the physics and perform collision checks on all entities + * + * @param elapsedUs Time since the last call + */ +static void marblesUpdatePhysics(int64_t elapsedUs) +{ + marblesEntity_t* prevEntity = NULL; + + // Loop over entities, handle movement, do collision checks + node_t* node = marbles->entities.first; + while (node != NULL) + { + bool cull = false; + marblesEntity_t* entity = (marblesEntity_t*)(node->val); + switch (entity->type) + { + case MARBLE: + { + if (entity->track) + { + entity->trackPos += entity->velocity; + if (!marblesCalculateTrackPos(entity->track->length, entity->track->points, entity->trackPos, + &entity->x, &entity->y) + && entity->track->temporary) + { + cull = true; + } + else + { + if (prevEntity && prevEntity->type == MARBLE && prevEntity->track + && prevEntity->track == entity->track) + { + // Bump this marble back until it doesn't collide with the last one + while (collideMarbles(prevEntity, entity) && entity->trackPos > 0) + { + // go in the reverse of the actual velocity + entity->trackPos += (entity->velocity >= 0 ? -1 : 1); + marblesCalculateTrackPos(entity->track->length, entity->track->points, entity->trackPos, + &entity->x, &entity->y); + } + } + } + } + else + { + // Move the marble along its trajectory + entity->x += entity->velocity * getCos1024(entity->angle) / 1024; + entity->y -= entity->velocity * getSin1024(entity->angle) / 1024; + + if (entity->x + MARBLE_R < 0 || entity->y + MARBLE_R < 0 || entity->x > TFT_WIDTH + MARBLE_R + || entity->y > TFT_HEIGHT + MARBLE_R) + { + // Shot marble is out of bounds, cull it + cull = true; + } + } + break; + } + + case MARBLE_GROUP: + { + break; + } + + case SHOOTER: + { + entity->angle = (int16_t)marbles->inputRotation; + + if (marbles->buttonState & PB_A) + { + if (marbles->shootCooldown == 0) + { + marblesEntity_t* proj = calloc(1, sizeof(marblesEntity_t)); + proj->type = MARBLE; + proj->x = entity->x; + proj->y = entity->y; + proj->velocity = 10; + proj->angle = entity->angle; + proj->marble.type = NORMAL; + proj->marble.color = entity->shooter.nextMarble.color; + entity->shooter.nextMarble.color + = marbles->level->colors[esp_random() % marbles->level->numColors]; + + int16_t mult = 300; + int16_t endX; + int16_t endY; + + // TODO: Use trigonometry to fix this insane loop + do + { + endX = entity->x + getCos1024(entity->angle) * mult / 1024; + endY = entity->y - getSin1024(entity->angle) * mult / 1024; + mult -= 5; + } while (endX < 0 || endX > TFT_WIDTH || endY < 0 || endY > TFT_HEIGHT); + + proj->trackPos = 0; + proj->track = calloc(1, sizeof(marbleTrack_t)); + proj->track->length = 2; + proj->track->points = malloc(proj->track->length * sizeof(point_t)); + proj->track->points[0].x = proj->x; + proj->track->points[0].y = proj->y; + proj->track->points[1].x = endX; + proj->track->points[1].y = endY; + proj->track->temporary = true; + + push(&marbles->entities, proj); + + // TODO constant-ize + marbles->shootCooldown = 300000; + } + } + break; + } + } + + if (cull) + { + node_t* tmp = node; + node = node->next; + marblesEntity_t* deleting = removeEntry(&marbles->entities, tmp); + + // If the entity has a temporary path, make sure that's freed too + if (deleting->track && deleting->track->temporary) + { + free(deleting->track); + } + + free(deleting); + } + else + { + prevEntity = entity; + node = node->next; + } + } +} + +/** + * @brief Draw the game level and entities + * + */ +static void marblesDrawLevel(void) +{ +#ifdef DRAW_PATH + for (uint8_t i = 0; i < marbles->level->track.length - 1; i++) + { + drawLine(marbles->level->track.points[i].x, marbles->level->track.points[i].y, + marbles->level->track.points[i + 1].x, marbles->level->track.points[i + 1].y, c111, 0); + } +#endif + + node_t* node = marbles->entities.first; + while (node != NULL) + { + marblesEntity_t* entity = (marblesEntity_t*)(node->val); + switch (entity->type) + { + case MARBLE: + { + drawCircleFilled(entity->x, entity->y, MARBLE_R, entity->marble.color); + break; + } + + case MARBLE_GROUP: + { + break; + } + + case SHOOTER: + { + drawWsg(&marbles->arrow18, entity->x - marbles->arrow18.w / 2, entity->y - marbles->arrow18.h / 2, + false, false, 359 - (entity->angle + 270) % 360); + floodFill(entity->x, entity->y, entity->shooter.nextMarble.color, entity->x - marbles->arrow18.w, + entity->y - marbles->arrow18.h, entity->x + marbles->arrow18.w, + entity->y + marbles->arrow18.h); + + int16_t mult = 300; + + int16_t endX; + int16_t endY; + + do + { + endX = entity->x + getCos1024(entity->angle) * mult / 1024; + endY = entity->y - getSin1024(entity->angle) * mult / 1024; + mult -= 5; + } while (endX < 0 || endX > TFT_WIDTH || endY < 0 || endY > TFT_HEIGHT); + // now... trace that ray to the edge of the screen and cut it as needed + drawLine(entity->x, entity->y, endX, endY, c511, 6); + break; + } + } + + node = node->next; + } +} + +/** + * @brief Loads a level into the mode structure + * + */ +static void marblesLoadLevel(void) +{ + marblesLevel_t* level = calloc(1, sizeof(marblesLevel_t)); + + ///////////////////////////////////////////////////////////////////// + // Simulate loading the level from a blob or other level generator // + ///////////////////////////////////////////////////////////////////// + level->numColors = 4; + level->colors = calloc(level->numColors, sizeof(paletteColor_t)); + // TODO pick more accessible colors + level->colors[0] = c500; // red + level->colors[1] = c550; // yellow + level->colors[2] = c050; // green + level->colors[3] = c055; // cyan + + level->numMarbles = 10; + level->numSpecials = 0; + level->specials = calloc(level->numSpecials, sizeof(marblesSpecial_t)); + + level->shooterLoc.x = TFT_WIDTH / 2; + level->shooterLoc.y = TFT_HEIGHT / 2; + + level->track.length = 4; + level->track.points = calloc(level->track.length, sizeof(point_t)); + + level->track.points[0].x = TFT_WIDTH; + level->track.points[0].y = TFT_HEIGHT / 4; + + level->track.points[1].x = TFT_WIDTH / 2; + level->track.points[1].y = TFT_HEIGHT / 4; + + level->track.points[2].x = TFT_WIDTH / 4; + level->track.points[2].y = TFT_HEIGHT / 2; + + level->track.points[3].x = TFT_WIDTH / 3; + level->track.points[3].y = TFT_HEIGHT - 1; + + marbles->level = level; + + ///////////////////////////////////////////////////////////////////// + // Handle actually setting up the game after loading the level // + ///////////////////////////////////////////////////////////////////// + marblesEntity_t* shooter = calloc(1, sizeof(marblesEntity_t)); + + shooter->type = SHOOTER; + shooter->x = level->shooterLoc.x; + shooter->y = level->shooterLoc.y; + shooter->angle = 90; + shooter->shooter.nextMarble.color = level->colors[esp_random() % level->numColors]; + push(&marbles->entities, shooter); + + for (uint8_t i = 0; i < 4; i++) + { + marblesEntity_t* marble = calloc(1, sizeof(marblesEntity_t)); + marble->track = &level->track; + marble->trackPos = 50 * (3 - i); + marble->velocity = 5; + marble->marble.type = NORMAL; + marble->marble.color = level->colors[i]; + push(&marbles->entities, marble); + } +} + +/** + * @brief Free all memory associated with the currently-loaded level + * + */ +static void marblesUnloadLevel(void) +{ + if (marbles->level->specials) + { + free(marbles->level->specials); + } + + free(marbles->level->track.points); + free(marbles->level->colors); + + free(marbles->level); + marbles->level = NULL; + + marblesEntity_t* val = NULL; + while (NULL != (val = pop(&marbles->entities))) + { + if (val->track && val->track->temporary) + { + free(val->track); + } + + free(val); + } +} + +/** + * This function is called from the main loop. It's pretty quick, but the + * timing may be inconsistent. + * + * @param elapsedUs The time elapsed since the last time this function was called. Use this value to determine when + * it's time to do things + */ +static void marblesMainLoop(int64_t elapsedUs) +{ + clearPxTft(); + + // Process button events + buttonEvt_t evt = {0}; + while (checkButtonQueueWrapper(&evt)) + { + marblesHandleButton(evt); + } + + switch (marbles->screen) + { + case MAIN_MENU: + { + drawMenuLogbook(marbles->menu, marbles->renderer, elapsedUs); + break; + } + + case IN_LEVEL: + { + getTouchJoystick(&marbles->inputRotation, NULL, NULL); + + if (marbles->shootCooldown < elapsedUs) + { + marbles->shootCooldown = 0; + } + else + { + marbles->shootCooldown -= elapsedUs; + } + + marblesUpdatePhysics(elapsedUs); + marblesDrawLevel(); + break; + } + } +} + +/** + * This function is called when the display driver wishes to update a + * section of the display. + * + * @param disp The display to draw to + * @param x the x coordiante that should be updated + * @param y the x coordiante that should be updated + * @param w the width of the rectangle to be updated + * @param h the height of the rectangle to be updated + * @param up update number + * @param upNum update number denominator + */ +static void marblesBackgroundDrawCallback(int16_t x, int16_t y, int16_t w, int16_t h, int16_t up, int16_t upNum) +{ + fillDisplayArea(x, y, x + w, y + h, c555); +} + +/** + * @brief Callback for when menu items are selected + * + * @param label The menu item that was selected or moved to + * @param selected true if the item was selected, false if it was moved to + * @param settingVal The value of the setting, if the menu item is a settings item + */ +static void marblesMenuCb(const char* label, bool selected, uint32_t settingVal) +{ + if (label == marblesPlay) + { + marblesLoadLevel(); + marbles->screen = IN_LEVEL; + } +} + +/** + * @brief Calculates the position of a point a certain distance along a track + * + * @param trackLength The number of points in the track path + * @param points An array of points defining the track's path + * @param position The position along the path, in thousandths-of-segment increments + * @param[out] x A pointer to an int to be updated to the result X-coordinate + * @param[out] y A pointer to an int to be updated to the result Y-coordinate + */ +static bool marblesCalculateTrackPos(uint8_t trackLength, const point_t* points, int32_t distance, int16_t* x, + int16_t* y) +{ + if (distance > (trackLength - 1) * 1000) + { + return false; + } + + int32_t startPoint = distance / 1000; + + if (startPoint == (trackLength - 1)) + { + // Return the last point if it's right at the end + *x = points[trackLength - 1].x; + *y = points[trackLength - 1].y; + } + else + { + int32_t n = distance % 1000; + // Otherwise, linear interpolation! + const point_t* p0 = (points + startPoint); + const point_t* p1 = (points + startPoint + 1); + + *x = ((999 - n) * p0->x + n * p1->x) / 1000; + *y = ((999 - n) * p0->y + n * p1->y) / 1000; + } + + return true; +} + +static bool collideMarbles(const marblesEntity_t* a, const marblesEntity_t* b) +{ + circle_t circleA, circleB; + circleA.x = a->x; + circleA.y = a->y; + circleA.radius = MARBLE_R; + + circleB.x = b->x; + circleB.y = b->y; + circleB.radius = MARBLE_R; + + return circleCircleIntersection(circleA, circleB); +} \ No newline at end of file diff --git a/main/modes/marbles/marbles.h b/main/modes/marbles/marbles.h new file mode 100644 index 000000000..4b202676a --- /dev/null +++ b/main/modes/marbles/marbles.h @@ -0,0 +1,8 @@ +#ifndef _MARBLES_H_ +#define _MARBLES_H_ + +#include "swadge2024.h" + +extern swadgeMode_t marblesMode; + +#endif \ No newline at end of file diff --git a/main/modes/mfpaint/mode_paint.c b/main/modes/mfpaint/mode_paint.c new file mode 100644 index 000000000..3ae95df0a --- /dev/null +++ b/main/modes/mfpaint/mode_paint.c @@ -0,0 +1,568 @@ +#include +#include + +#include "esp_log.h" + +#include "swadge2024.h" +#include "hdw-btn.h" +#include "menu.h" +#include "menuLogbookRenderer.h" +#include "mainMenu.h" +#include "swadge2024.h" +#include "settingsManager.h" +#include "shapes.h" +#include "math.h" + +#include "settingsManager.h" + +#include "mode_paint.h" +#include "paint_common.h" +#include "paint_util.h" +#include "paint_draw.h" +#include "paint_gallery.h" +#include "paint_share.h" +#include "paint_nvs.h" + +const char paintTitle[] = "MFPaint"; +const char menuOptDraw[] = "Draw"; +const char menuOptHelp[] = "Tutorial"; +const char menuOptGallery[] = "Gallery"; +const char menuOptNetwork[] = "Sharing"; +const char menuOptShare[] = "Share"; +const char menuOptReceive[] = "Receive"; +const char menuOptSettings[] = "Settings"; + +const char menuOptLeds[] = "LEDs"; +const char menuOptBlink[] = "Blink Picks"; + +const char menuOptLedsOn[] = "LEDs: On"; +const char menuOptLedsOff[] = "LEDs: Off"; +const char menuOptBlinkOn[] = "Blink Picks: On"; +const char menuOptBlinkOff[] = "Blink Picks: Off"; +const char menuOptEraseData[] = "Erase: All"; +char menuOptEraseSlot[] = "Erase: Slot 1"; +const char menuOptCancelErase[] = "Confirm: No!"; +const char menuOptConfirmErase[] = "Confirm: Yes"; + +const char menuOptExit[] = "Exit"; +const char menuOptBack[] = "Back"; + +// Mode struct function declarations +void paintEnterMode(void); +void paintExitMode(void); +void paintMainLoop(int64_t elapsedUs); +void paintButtonCb(buttonEvt_t* evt); + +swadgeMode_t modePaint = { + .modeName = paintTitle, + .wifiMode = ESP_NOW, + .overrideUsb = false, + .usesAccelerometer = false, + .usesThermometer = false, + .fnEnterMode = paintEnterMode, + .fnExitMode = paintExitMode, + .fnMainLoop = paintMainLoop, + .fnAudioCallback = NULL, + .fnBackgroundDrawCallback = NULL, + .fnEspNowRecvCb = NULL, + .fnEspNowSendCb = NULL, + .fnAdvancedUSB = NULL, +}; + +// Menu callback declaration +void paintMenuCb(const char* label, bool selected, uint32_t value); + +// Util function declarations + +void paintDeleteAllData(void); +void paintMenuInitialize(void); +void paintMenuPrevEraseOption(void); +void paintMenuNextEraseOption(void); +void paintSetupMainMenu(void); + +// Mode struct function implemetations + +void paintEnterMode(void) +{ + PAINT_LOGI("Allocating %" PRIu32 " bytes for paintMenu...", (uint32_t)sizeof(paintMainMenu_t)); + paintMenu = calloc(1, sizeof(paintMainMenu_t)); + + loadFont("logbook.font", &(paintMenu->menuFont), false); + + paintMenu->menu = initMenu(paintTitle, paintMenuCb); + paintMenu->menuRenderer = initMenuLogbookRenderer(&paintMenu->menuFont); + + paintMenuInitialize(); +} + +void paintExitMode(void) +{ + PAINT_LOGD("Exiting"); + + // Cleanup any sub-modes based on paintMenu->screen + paintReturnToMainMenu(); + + deinitMenu(paintMenu->menu); + deinitMenuLogbookRenderer(paintMenu->menuRenderer); + freeFont(&(paintMenu->menuFont)); + + free(paintMenu); + paintMenu = NULL; +} + +void paintMainLoop(int64_t elapsedUs) +{ + // Handle all input frst regardless of screen + buttonEvt_t evt = {0}; + while (checkButtonQueueWrapper(&evt)) + { + paintButtonCb(&evt); + } + + switch (paintMenu->screen) + { + case PAINT_MENU: + { + if (paintMenu->enableScreensaver && getScreensaverTimeSetting() != 0) + { + paintMenu->idleTimer += elapsedUs; + } + + if (getScreensaverTimeSetting() != 0 && paintMenu->idleTimer >= (getScreensaverTimeSetting() * 1000000)) + { + PAINT_LOGI("Selected Gallery"); + paintGallerySetup(true); + paintGallery->returnScreen = paintMenu->screen; + paintMenu->screen = PAINT_GALLERY; + } + else + { + drawMenuLogbook(paintMenu->menu, paintMenu->menuRenderer, elapsedUs); + } + break; + } + + case PAINT_DRAW: + case PAINT_HELP: + { + paintDrawScreenMainLoop(elapsedUs); + break; + } + + case PAINT_SHARE: + // Implemented in a different mode + break; + + case PAINT_RECEIVE: + // Implemented in a different mode + break; + + case PAINT_GALLERY: + { + paintGalleryMainLoop(elapsedUs); + break; + } + } +} + +void paintButtonCb(buttonEvt_t* evt) +{ + paintMenu->idleTimer = 0; + + switch (paintMenu->screen) + { + case PAINT_MENU: + { + paintMenu->menu = menuButton(paintMenu->menu, *evt); + break; + } + + /*case PAINT_SETTINGS_MENU: + { + if (evt->down) + { + if (evt->button == PB_LEFT || evt->button == PB_RIGHT) + { + if (menuOptCancelErase == selectedOption) + { + paintMenu->eraseDataConfirm = true; + paintSetupSettingsMenu(false); + } + else if (menuOptConfirmErase == selectedOption) + { + paintMenu->eraseDataConfirm = false; + paintSetupSettingsMenu(false); + } + else if (menuOptEraseSlot == selectedOption || menuOptEraseData == selectedOption) + { + paintMenu->settingsMenuSelection = paintMenu->menu->selectedRow; + if (evt->button == PB_LEFT) + { + paintMenuPrevEraseOption(); + paintSetupSettingsMenu(false); + } + else if (evt->button == PB_RIGHT) + { + paintMenuNextEraseOption(); + paintSetupSettingsMenu(false); + } + } + } + else if (evt->button == PB_B) + { + if (paintMenu->eraseDataSelected) + { + paintMenu->eraseDataSelected = false; + paintSetupSettingsMenu(false); + } + else + { + paintMenu->screen = PAINT_MENU; + paintSetupMainMenu(false); + } + } + else + { + meleeMenuButton(paintMenu->menu, evt->button); + selectedOption = paintMenu->menu->rows[paintMenu->menu->selectedRow]; + } + + if (paintMenu->eraseDataSelected && menuOptCancelErase != selectedOption && + menuOptConfirmErase != selectedOption) + { + // If the confirm-erase option is not selected, reset eraseDataConfirm and redraw the menu + paintMenu->settingsMenuSelection = paintMenu->menu->selectedRow; + paintMenu->eraseDataSelected = false; + paintMenu->eraseDataConfirm = false; + paintSetupSettingsMenu(false); + } + } + break; + }*/ + + case PAINT_DRAW: + case PAINT_HELP: + { + paintDrawScreenButtonCb(evt); + break; + } + + case PAINT_GALLERY: + { + paintGalleryModeButtonCb(evt); + break; + } + + case PAINT_SHARE: + case PAINT_RECEIVE: + // Handled in a different mode + break; + } +} + +// Util function implementations + +void paintMenuInitialize(void) +{ + paintMenu->idleTimer = 0; + paintMenu->screen = PAINT_MENU; + + paintSetupMainMenu(); +} + +void paintSetupMainMenu(void) +{ + int32_t index; + paintLoadIndex(&index); + + addSingleItemToMenu(paintMenu->menu, menuOptDraw); + + if (paintGetAnySlotInUse(index)) + { + // Only add "gallery" if there's something to view + paintMenu->enableScreensaver = true; + addSingleItemToMenu(paintMenu->menu, menuOptGallery); + } + else + { + paintMenu->enableScreensaver = false; + } + + paintMenu->menu = startSubMenu(paintMenu->menu, menuOptNetwork); + if (paintGetAnySlotInUse(index)) + { + addSingleItemToMenu(paintMenu->menu, menuOptShare); + } + addSingleItemToMenu(paintMenu->menu, menuOptReceive); + paintMenu->menu = endSubMenu(paintMenu->menu); + + addSingleItemToMenu(paintMenu->menu, menuOptHelp); + + paintMenu->menu = startSubMenu(paintMenu->menu, menuOptSettings); + addSettingsItemToMenu(paintMenu->menu, menuOptLeds, paintGetEnableLedsBounds(), paintGetEnableLeds()); + addSettingsItemToMenu(paintMenu->menu, menuOptBlink, paintGetEnableBlinkBounds(), paintGetEnableBlink()); + + /*if (paintMenu->eraseDataSelected) + { + if (paintMenu->eraseDataConfirm) + { + addRowToMeleeMenu(paintMenu->menu, menuOptConfirmErase); + } + else + { + addRowToMeleeMenu(paintMenu->menu, menuOptCancelErase); + } + } + else + { + if (paintMenu->eraseSlot == PAINT_SAVE_SLOTS) + { + addRowToMeleeMenu(paintMenu->menu, menuOptEraseData); + } + else + { + snprintf(menuOptEraseSlot, sizeof(menuOptEraseSlot), "Erase: Slot %d", paintMenu->eraseSlot % + PAINT_SAVE_SLOTS + 1); addRowToMeleeMenu(paintMenu->menu, menuOptEraseSlot); + } + }*/ + paintMenu->menu = endSubMenu(paintMenu->menu); + + addSingleItemToMenu(paintMenu->menu, menuOptExit); +} + +void paintMenuPrevEraseOption(void) +{ + int32_t index; + paintLoadIndex(&index); + + if (paintGetAnySlotInUse(index)) + { + PAINT_LOGD("A slot is in use"); + uint8_t prevSlot = paintGetPrevSlotInUse(index, paintMenu->eraseSlot); + + PAINT_LOGD("Current eraseSlot: %d, prev: %d", paintMenu->eraseSlot, prevSlot); + if (paintMenu->eraseSlot != PAINT_SAVE_SLOTS && prevSlot >= paintMenu->eraseSlot) + { + PAINT_LOGD("Wrapped, moving to All"); + // we wrapped around + paintMenu->eraseSlot = PAINT_SAVE_SLOTS; + } + else + { + paintMenu->eraseSlot = prevSlot; + } + } + else + { + PAINT_LOGD("No in-use slots, moving to All"); + paintMenu->eraseSlot = PAINT_SAVE_SLOTS; + } +} + +void paintMenuNextEraseOption(void) +{ + int32_t index; + paintLoadIndex(&index); + + if (paintGetAnySlotInUse(index)) + { + PAINT_LOGD("A slot is in use"); + uint8_t nextSlot = paintGetNextSlotInUse( + index, (paintMenu->eraseSlot == PAINT_SAVE_SLOTS) ? PAINT_SAVE_SLOTS - 1 : paintMenu->eraseSlot); + PAINT_LOGD("Current eraseSlot: %d, next: %d", paintMenu->eraseSlot, nextSlot); + if (paintMenu->eraseSlot != PAINT_SAVE_SLOTS && nextSlot <= paintMenu->eraseSlot) + { + PAINT_LOGD("Wrapped, moving to All"); + // we wrapped around + paintMenu->eraseSlot = PAINT_SAVE_SLOTS; + } + else + { + paintMenu->eraseSlot = nextSlot; + } + } + else + { + PAINT_LOGD("No in-use slots, moving to All"); + paintMenu->eraseSlot = PAINT_SAVE_SLOTS; + } +} + +void paintMenuCb(const char* opt, bool selected, uint32_t value) +{ + if (selected) + { + if (opt == menuOptDraw) + { + PAINT_LOGI("Selected Draw"); + paintMenu->screen = PAINT_DRAW; + paintDrawScreenSetup(); + } + else if (opt == menuOptGallery) + { + PAINT_LOGI("Selected Gallery"); + paintMenu->screen = PAINT_GALLERY; + paintGallerySetup(false); + } + else if (opt == menuOptHelp) + { + PAINT_LOGE("Selected Help"); + paintMenu->screen = PAINT_HELP; + paintTutorialSetup(); + paintDrawScreenSetup(); + } + else if (opt == menuOptShare) + { + PAINT_LOGI("Selected Share"); + paintMenu->screen = PAINT_SHARE; + switchToSwadgeMode(&modePaintShare); + } + else if (opt == menuOptReceive) + { + PAINT_LOGI("Selected Receive"); + paintMenu->screen = PAINT_RECEIVE; + switchToSwadgeMode(&modePaintReceive); + } + else if (opt == menuOptExit) + { + PAINT_LOGI("Selected Exit"); + switchToSwadgeMode(&mainMenuMode); + } + } + else + { + if (opt == menuOptLeds) + { + paintSetEnableLeds(value); + } + else if (opt == menuOptBlink) + { + paintSetEnableBlink(value); + } + } +} + +/*void paintSettingsMenuCb(const char* opt) +{ + paintMenu->settingsMenuSelection = paintMenu->menu->selectedRow; + + int32_t index; + if (opt == menuOptLedsOff) + { + // Enable the LEDs + paintLoadIndex(&index); + index |= PAINT_ENABLE_LEDS; + paintSaveIndex(index); + } + else if (opt == menuOptLedsOn) + { + paintLoadIndex(&index); + index &= ~PAINT_ENABLE_LEDS; + paintSaveIndex(index); + } + else if (opt == menuOptBlinkOff) + { + paintLoadIndex(&index); + index |= PAINT_ENABLE_BLINK; + paintSaveIndex(index); + } + else if (opt == menuOptBlinkOn) + { + paintLoadIndex(&index); + index &= ~PAINT_ENABLE_BLINK; + paintSaveIndex(index); + } + else if (opt == menuOptEraseData) + { + paintMenu->eraseDataSelected = true; + } + else if (opt == menuOptEraseSlot) + { + paintMenu->eraseDataSelected = true; + paintMenu->eraseDataConfirm = false; + } + else if (opt == menuOptConfirmErase) + { + if (paintMenu->eraseSlot == PAINT_SAVE_SLOTS) + { + paintDeleteAllData(); + } + else + { + paintLoadIndex(&index); + paintDeleteSlot(&index, paintMenu->eraseSlot); + paintMenuNextEraseOption(); + } + paintMenu->enableScreensaver = paintMenu->enableScreensaver && paintGetAnySlotInUse(index); + paintMenu->eraseDataConfirm = false; + paintMenu->eraseDataSelected = false; + } + else if (opt == menuOptCancelErase) + { + paintMenu->eraseDataSelected = false; + paintMenu->eraseDataConfirm = false; + } + else if (opt == menuOptBack) + { + PAINT_LOGI("Selected Back"); + paintSetupMainMenu(false); + paintMenu->screen = PAINT_MENU; + return; + } + + paintSetupSettingsMenu(false); +}*/ + +void paintReturnToMainMenu(void) +{ + switch (paintMenu->screen) + { + case PAINT_MENU: + case PAINT_SHARE: + case PAINT_RECEIVE: + break; + + case PAINT_DRAW: + paintDrawScreenCleanup(); + break; + + case PAINT_GALLERY: + if (paintGallery->screensaverMode) + { + paintMenu->screen = paintGallery->returnScreen; + paintGalleryCleanup(); + if (paintMenu->screen == PAINT_MENU) + { + paintSetupMainMenu(); + } + return; + } + else + { + paintGalleryCleanup(); + } + break; + + case PAINT_HELP: + paintTutorialCleanup(); + paintDrawScreenCleanup(); + break; + } + + paintMenu->screen = PAINT_MENU; + paintMenu->idleTimer = 0; + paintSetupMainMenu(); +} + +void paintDeleteAllData(void) +{ + int32_t index; + paintLoadIndex(&index); + + for (uint8_t i = 0; i < PAINT_SAVE_SLOTS; i++) + { + paintDeleteSlot(&index, i); + } + + paintDeleteIndex(); +} diff --git a/main/modes/mfpaint/mode_paint.h b/main/modes/mfpaint/mode_paint.h new file mode 100644 index 000000000..2c5bac5b8 --- /dev/null +++ b/main/modes/mfpaint/mode_paint.h @@ -0,0 +1,11 @@ +#ifndef _MODE_PAINT_H_ +#define _MODE_PAINT_H_ + +#include "swadge2024.h" +#include "hdw-bzr.h" + +extern swadgeMode_t modePaint; + +void paintReturnToMainMenu(void); + +#endif diff --git a/main/modes/mfpaint/paint_brush.c b/main/modes/mfpaint/paint_brush.c new file mode 100644 index 000000000..55ac92d69 --- /dev/null +++ b/main/modes/mfpaint/paint_brush.c @@ -0,0 +1,176 @@ +#include "paint_brush.h" + +#include +#include + +#include "paint_common.h" +#include "paint_util.h" +#include "shapes.h" + +void paintDrawSquarePen(paintCanvas_t* canvas, point_t* points, uint8_t numPoints, uint16_t size, paletteColor_t col) +{ + drawRectFilledScaled(points[0].x - (size / 2), points[0].y - (size / 2), points[0].x + ((size + 1) / 2), + points[0].y + ((size + 1) / 2), col, canvas->x, canvas->y, canvas->xScale, canvas->yScale); +} + +void paintDrawCirclePen(paintCanvas_t* canvas, point_t* points, uint8_t numPoints, uint16_t size, paletteColor_t col) +{ + drawCircleFilledScaled(points[0].x, points[0].y, size, col, canvas->x, canvas->y, canvas->xScale, canvas->yScale); + + // fill out the circle if it's very small + if (size == 1) + { + setPxScaled(points[0].x, points[0].y - 1, col, canvas->x, canvas->y, canvas->xScale, canvas->yScale); + setPxScaled(points[0].x, points[0].y + 1, col, canvas->x, canvas->y, canvas->xScale, canvas->yScale); + } +} + +void paintDrawLine(paintCanvas_t* canvas, point_t* points, uint8_t numPoints, uint16_t size, paletteColor_t col) +{ + for (int16_t x = -size / 2; x < (size + 1) / 2; x++) + { + for (int16_t y = -size / 2; y < (size + 1) / 2; y++) + { + drawLineScaled(points[0].x + x, points[0].y + y, points[1].x + x, points[1].y + y, col, 0, canvas->x, + canvas->y, canvas->xScale, canvas->yScale); + } + } +} + +void paintDrawCurve(paintCanvas_t* canvas, point_t* points, uint8_t numPoints, uint16_t size, paletteColor_t col) +{ + for (int16_t x = -size / 2; x < (size + 1) / 2; x++) + { + for (int16_t y = -size / 2; y < (size + 1) / 2; y++) + { + drawCubicBezierScaled(points[0].x + x, points[0].y + y, points[1].x + x, points[1].y + y, points[2].x + x, + points[2].y + y, points[3].x + x, points[3].y + y, col, canvas->x, canvas->y, + canvas->xScale, canvas->yScale); + } + } +} + +void paintDrawRectangle(paintCanvas_t* canvas, point_t* points, uint8_t numPoints, uint16_t size, paletteColor_t col) +{ + uint16_t x0 = (points[0].x > points[1].x) ? points[1].x : points[0].x; + uint16_t y0 = (points[0].y > points[1].y) ? points[1].y : points[0].y; + uint16_t x1 = (points[0].x > points[1].x) ? points[0].x : points[1].x; + uint16_t y1 = (points[0].y > points[1].y) ? points[0].y : points[1].y; + + point_t tmpPoints[2]; + // x0, y0 -> x0, y1 + tmpPoints[0].x = x0; + tmpPoints[0].y = y0; + tmpPoints[1].x = x0; + tmpPoints[1].y = y1; + paintDrawLine(canvas, tmpPoints, 2, size, col); + + // x0, y0 -> x1, y0 + tmpPoints[1].x = x1; + tmpPoints[1].y = y0; + paintDrawLine(canvas, tmpPoints, 2, size, col); + + // x0, y1 -> x1, y1 + tmpPoints[0].y = y1; + tmpPoints[1].y = y1; + paintDrawLine(canvas, tmpPoints, 2, size, col); + + // x1, y0 -> x1, y1 + tmpPoints[0].x = x1; + tmpPoints[0].y = y0; + paintDrawLine(canvas, tmpPoints, 2, size, col); +} + +void paintDrawFilledRectangle(paintCanvas_t* canvas, point_t* points, uint8_t numPoints, uint16_t size, + paletteColor_t col) +{ + uint16_t x0 = (points[0].x > points[1].x) ? points[1].x : points[0].x; + uint16_t y0 = (points[0].y > points[1].y) ? points[1].y : points[0].y; + uint16_t x1 = (points[0].x > points[1].x) ? points[0].x : points[1].x; + uint16_t y1 = (points[0].y > points[1].y) ? points[0].y : points[1].y; + + // This function takes care of its own scaling because it's very easy and will save a lot of unnecessary draws + drawRectFilledScaled(x0, y0, x1 + 1, y1 + 1, col, canvas->x, canvas->y, canvas->xScale, canvas->yScale); +} + +void paintDrawCircle(paintCanvas_t* canvas, point_t* points, uint8_t numPoints, uint16_t size, paletteColor_t col) +{ + uint16_t dX = abs(points[0].x - points[1].x); + uint16_t dY = abs(points[0].y - points[1].y); + uint16_t r = (uint16_t)(sqrt(dX * dX + dY * dY) + 0.5); + + for (int16_t x = -size / 2; x < (size + 1) / 2; x++) + { + for (int16_t y = -size / 2; y < (size + 1) / 2; y++) + { + drawCircleScaled(points[0].x + x, points[0].y + y, r, col, canvas->x, canvas->y, canvas->xScale, + canvas->yScale); + } + } +} + +void paintDrawFilledCircle(paintCanvas_t* canvas, point_t* points, uint8_t numPoints, uint16_t size, paletteColor_t col) +{ + uint16_t dX = abs(points[0].x - points[1].x); + uint16_t dY = abs(points[0].y - points[1].y); + uint16_t r = (uint16_t)(sqrt(dX * dX + dY * dY) + 0.5); + + drawCircleFilledScaled(points[0].x, points[0].y, r, col, canvas->x, canvas->y, canvas->xScale, canvas->yScale); +} + +void paintDrawEllipse(paintCanvas_t* canvas, point_t* points, uint8_t numPoints, uint16_t size, paletteColor_t col) +{ + uint16_t x0 = (points[0].x > points[1].x) ? points[1].x : points[0].x; + uint16_t y0 = (points[0].y > points[1].y) ? points[1].y : points[0].y; + uint16_t x1 = (points[0].x > points[1].x) ? points[0].x : points[1].x; + uint16_t y1 = (points[0].y > points[1].y) ? points[0].y : points[1].y; + + pxStack_t tmpPxs; + // TODO maybe we shouldn't use a heap-allocated pxStack here + initPxStack(&tmpPxs); + + for (int16_t x = 0; x < size; x++) + { + for (int16_t y = 0; y < size; y++) + { + if (x * 2 >= (x1 - x0 + 1) || y * 2 >= (y1 - y0 + 1)) + { + // don't draw a ridiculously large ellipse + continue; + } + // for some reason, plotting an ellipse also plots 2 extra points outside of the ellipse + // let's just work around that + pushPxScaled(&tmpPxs, x0 + x + ((x1 - x) - (x0 + x) + 1) / 2, y0 - 2 + y, canvas->x, canvas->y, + canvas->xScale, canvas->yScale); + pushPxScaled(&tmpPxs, x0 + x + ((x1 - x) - (x0 + x) + 1) / 2, y1 + 2 - y, canvas->x, canvas->y, + canvas->xScale, canvas->yScale); + + drawEllipseRectScaled(x0 + x, y0 + y, x1 - x, y1 - y, col, canvas->x, canvas->y, canvas->xScale, + canvas->yScale); + + while (popPxScaled(&tmpPxs, canvas->xScale, canvas->yScale)) + ; + } + } + freePxStack(&tmpPxs); +} + +void paintDrawPolygon(paintCanvas_t* canvas, point_t* points, uint8_t numPoints, uint16_t size, paletteColor_t col) +{ + for (uint8_t i = 0; i < numPoints - 1; i++) + { + paintDrawLine(canvas, points + i, 2, size, col); + } +} + +void paintDrawSquareWave(paintCanvas_t* canvas, point_t* points, uint8_t numPoints, uint16_t size, paletteColor_t col) +{ + paintPlotSquareWave(points[0].x, points[0].y, points[1].x, points[1].y, size, col, canvas->x, canvas->y, + canvas->xScale, canvas->yScale); +} + +void paintDrawPaintBucket(paintCanvas_t* canvas, point_t* points, uint8_t numPoints, uint16_t size, paletteColor_t col) +{ + floodFill(canvas->x + canvas->xScale * points[0].x, canvas->y + canvas->yScale * points[0].y, col, canvas->x, + canvas->y, canvas->x + canvas->xScale * canvas->w, canvas->y + canvas->yScale * canvas->h); +} diff --git a/main/modes/mfpaint/paint_brush.h b/main/modes/mfpaint/paint_brush.h new file mode 100644 index 000000000..938abb8a1 --- /dev/null +++ b/main/modes/mfpaint/paint_brush.h @@ -0,0 +1,84 @@ +#ifndef _PAINT_BRUSH_H_ +#define _PAINT_BRUSH_H_ + +#include +#include + +#include "wsg.h" + +#include "paint_type.h" + +/** + * Defines different brush behaviors for when A is pressed + */ +typedef enum +{ + // The brush is drawn whenever A is pressed or held and dragged + HOLD_DRAW, + + // The brush requires a number of points to be picked first, and then it is drawn + PICK_POINT, + + // The brush requires a number of points that always connect back to the first point + PICK_POINT_LOOP, +} brushMode_t; + +typedef struct +{ + /** + * @brief The behavior mode of this brush + */ + brushMode_t mode; + + /** + * @brief The number of points this brush can use + */ + uint8_t maxPoints; + + /** + * @brief The minimum size (e.g. stroke width) of the brush + */ + uint16_t minSize; + + /** + * @brief The maximum size of the brush + */ + uint16_t maxSize; + + /** + * @brief The display name of this brush + */ + char* name; + + /** + * @brief The base name of the toolbar icon. + * The icon sprites will be loaded from {iconName}_active.wsg and {iconName}_inactive.wsg + */ + char* iconName; + + /// @brief The icon to be shown in the toolbar when the tool is selected + wsg_t iconActive; + + /// @brief The icon to be shown in the toolbar when the tool is not selected + wsg_t iconInactive; + + /** + * @brief Called when all necessary points have been selected and the final shape should be drawn + */ + void (*fnDraw)(paintCanvas_t* canvas, point_t* points, uint8_t numPoints, uint16_t size, paletteColor_t col); +} brush_t; + +void paintDrawSquarePen(paintCanvas_t*, point_t*, uint8_t, uint16_t, paletteColor_t); +void paintDrawCirclePen(paintCanvas_t*, point_t*, uint8_t, uint16_t, paletteColor_t); +void paintDrawLine(paintCanvas_t*, point_t*, uint8_t, uint16_t, paletteColor_t); +void paintDrawCurve(paintCanvas_t*, point_t*, uint8_t, uint16_t, paletteColor_t); +void paintDrawRectangle(paintCanvas_t*, point_t*, uint8_t, uint16_t, paletteColor_t); +void paintDrawFilledRectangle(paintCanvas_t*, point_t*, uint8_t, uint16_t, paletteColor_t); +void paintDrawCircle(paintCanvas_t*, point_t*, uint8_t, uint16_t, paletteColor_t); +void paintDrawFilledCircle(paintCanvas_t*, point_t*, uint8_t, uint16_t, paletteColor_t); +void paintDrawEllipse(paintCanvas_t*, point_t*, uint8_t, uint16_t, paletteColor_t); +void paintDrawPolygon(paintCanvas_t*, point_t*, uint8_t, uint16_t, paletteColor_t); +void paintDrawSquareWave(paintCanvas_t*, point_t*, uint8_t, uint16_t, paletteColor_t); +void paintDrawPaintBucket(paintCanvas_t*, point_t*, uint8_t, uint16_t, paletteColor_t); + +#endif diff --git a/main/modes/mfpaint/paint_common.c b/main/modes/mfpaint/paint_common.c new file mode 100644 index 000000000..cdd1fbc9e --- /dev/null +++ b/main/modes/mfpaint/paint_common.c @@ -0,0 +1,3 @@ +#include "paint_common.h" + +paintMainMenu_t* paintMenu; diff --git a/main/modes/mfpaint/paint_common.h b/main/modes/mfpaint/paint_common.h new file mode 100644 index 000000000..62cc70997 --- /dev/null +++ b/main/modes/mfpaint/paint_common.h @@ -0,0 +1,441 @@ +#ifndef _PAINT_COMMON_H_ +#define _PAINT_COMMON_H_ + +#include + +#include "esp_log.h" +#include "hdw-tft.h" +#include "swadge2024.h" +#include "menu.h" +#include "wheel_menu.h" +#include "p2pConnection.h" +#include "linked_list.h" +#include "geometry.h" + +#include "px_stack.h" +#include "paint_type.h" +#include "paint_brush.h" + +#define PAINT_LOGV(...) ESP_LOGV("Paint", __VA_ARGS__) +#define PAINT_LOGD(...) ESP_LOGD("Paint", __VA_ARGS__) +#define PAINT_LOGI(...) ESP_LOGI("Paint", __VA_ARGS__) +#define PAINT_LOGW(...) ESP_LOGW("Paint", __VA_ARGS__) +#define PAINT_LOGE(...) ESP_LOGE("Paint", __VA_ARGS__) + +//////// Data Constants + +// The total number of save slots available +#define PAINT_SAVE_SLOTS 4 + +// Whether to have the LEDs show the current colors +#define PAINT_ENABLE_LEDS (0x0001 << (PAINT_SAVE_SLOTS * 2)) + +// Whether to play SFX on drawing, etc. +#define PAINT_ENABLE_SFX (0x0002 << (PAINT_SAVE_SLOTS * 2)) + +// Whether to play background music +#define PAINT_ENABLE_BGM (0x0004 << (PAINT_SAVE_SLOTS * 2)) + +// Whether to enable blinking pick points, and any other potentilaly annoying things +#define PAINT_ENABLE_BLINK (0x0008 << (PAINT_SAVE_SLOTS * 2)) + +// Default to LEDs, SFX, and music on, with slot 0 marked as most recent +#define PAINT_DEFAULTS \ + (PAINT_ENABLE_LEDS | PAINT_ENABLE_SFX | PAINT_ENABLE_BGM | PAINT_ENABLE_BLINK \ + | (PAINT_SAVE_SLOTS << PAINT_SAVE_SLOTS)) + +// Mask for the index that includes everything except the most-recent index +#define PAINT_MASK_NOT_RECENT \ + (PAINT_ENABLE_LEDS | PAINT_ENABLE_SFX | PAINT_ENABLE_BGM | PAINT_ENABLE_BLINK | ((1 << PAINT_SAVE_SLOTS) - 1)) + +// The size of the buffer for loading/saving the image. Each chunk is saved as a separate blob in NVS +#define PAINT_SAVE_CHUNK_SIZE 1024 + +#define PAINT_SHARE_PX_PACKET_LEN (P2P_MAX_DATA_LEN - 3 - 11) +#define PAINT_SHARE_PX_PER_PACKET PAINT_SHARE_PX_PACKET_LEN * 2 + +//////// Draw Screen Layout Constants and Colors + +#define PAINT_DEFAULT_CANVAS_WIDTH 70 +#define PAINT_DEFAULT_CANVAS_HEIGHT 60 + +// Keep at least 3px free above and below the toolbar text +#define PAINT_TOOLBAR_TEXT_PADDING_Y 3 + +#define PAINT_TOOLBAR_FONT "ibm_vga8.font" +// #define PAINT_SHARE_TOOLBAR_FONT "radiostars.font" +#define PAINT_SHARE_TOOLBAR_FONT "ibm_vga8.font" +// #define PAINT_SAVE_MENU_FONT "radiostars.font" +#define PAINT_SAVE_MENU_FONT "ibm_vga8.font" +// #define PAINT_SMALL_FONT "tom_thumb.font" +#define PAINT_SMALL_FONT "ibm_vga8.font" + +#define PAINT_TOOLBAR_BG c444 + +// Dimensions of the color boxes in the palette +#define PAINT_COLORBOX_W 9 +#define PAINT_COLORBOX_H 9 + +// Spacing between the tool icons and the size, and the size and pick point counts +#define TOOL_INFO_TEXT_MARGIN_Y 6 + +#define PAINT_COLORBOX_SHADOW_TOP c000 +#define PAINT_COLORBOX_SHADOW_BOTTOM c111 + +// The screen's corner radius in pixels +#define TFT_CORNER_RADIUS 40 + +// Vertical margin between each color box +#define PAINT_COLORBOX_MARGIN_TOP 2 +// Minimum margin to the left and right of each color box +#define PAINT_COLORBOX_MARGIN_X 4 + +// X and Y position of the active color boxes (foreground/background color) +#define PAINT_ACTIVE_COLOR_X (TFT_CORNER_RADIUS / 2 + PAINT_COLORBOX_MARGIN_X) +#define PAINT_ACTIVE_COLOR_Y (TFT_HEIGHT - PAINT_COLORBOX_H * 2 - PAINT_COLORBOX_MARGIN_TOP) + +// Color picker stuff +#define PAINT_COLOR_PICKER_MIN_BAR_H 6 +#define PAINT_COLOR_PICKER_BAR_W 6 + +//////// Help layout stuff + +// Number of lines of text to make room for below the canvas +#define PAINT_HELP_TEXT_LINES 4 + +//////// Macros + +// Calculates previous and next items with wraparound +#define PREV_WRAP(i, count) ((i) == 0 ? (count)-1 : (i - 1)) +#define NEXT_WRAP(i, count) ((i + 1) % count) + +//////// Various Constants + +#define PAINT_MAX_BRUSH_SWIPE 16 + +// hold button for .3s to begin repeating +#define BUTTON_REPEAT_TIME 300000 + +// 10 seconds to go to gallery screensaver +#define PAINT_SCREENSAVER_TIMEOUT 10000000 + +#define BLINK_TIME_ON 500000 +#define BLINK_TIME_OFF 200000 + +/// @brief Struct encapsulating a cursor on the screen +typedef struct +{ + /// @brief The sprite for drawing the cursor + const wsg_t* sprite; + + /// @brief The position of the top-left corner of the sprite, relative to the cursor position + int8_t spriteOffsetX, spriteOffsetY; + + /// @brief A pixel stack of all pixels covered up by the cursor in its current position + pxStack_t underPxs; + + /// @brief The canvas X and Y coordinates of the cursor + int16_t x, y; + + /// @brief True if the cursor should be drawn, false if not + bool show; + + /// @brief True when the cursor state has changed and it needs to be redrawn + bool redraw; +} paintCursor_t; + +/// @brief Struct encapsulating all info for a single player +typedef struct +{ + /// @brief Pointer to the player's selected brush definition + const brush_t* brushDef; + + /// @brief The brush width or variant, depending on the brush definition + uint8_t brushWidth; + + /// @brief A stack containing the points for the current pending draw action + pxStack_t pickPoints; + + /// @brief The player's cursor information + paintCursor_t cursor; + + /// @brief The player's selected foreground and background colors + paletteColor_t fgColor, bgColor; +} paintArtist_t; + +typedef struct +{ + led_t leds[CONFIG_NUM_LEDS]; + + paintCanvas_t canvas; + + // Margins that define the space the canvas may be placed within. + uint16_t marginTop, marginLeft, marginBottom, marginRight; + + // Font for drawing tool info (width, pick points) + font_t toolbarFont; + // Font for drawing save / load / clear / exit menu + font_t saveMenuFont; + // Small font for small things (text above color picker gradient bars) + font_t smallFont; + + // Index keeping track of which slots are in use and the most recent slot + int32_t index; + + // All shared state for 1 or 2 players + paintArtist_t artist[2]; + + // The generated cursor sprite + wsg_t cursorWsg; + + // The "brush size" indicator sprite + wsg_t brushSizeWsg; + + // The "picks remaining" sprite + wsg_t picksWsg; + + // The 9x9 arrow + wsg_t smallArrowWsg; + + // The 12x12 arrow + wsg_t bigArrowWsg; + + // Icon to indicate free slot + wsg_t newfileWsg; + + // Icon to indicate used slot + wsg_t overwriteWsg; + + //////// Local-only UI state + + // Which mode will be used to interpret button presses + paintButtonMode_t buttonMode; + + // Whether or not A is currently held + bool aHeld; + + // flag so that an a press shorter than 1 frame always gets handled + bool aPress; + + // When true, this is the initial D-pad button down. + // If set, the cursor will move by one pixel and then it will be cleared. + // The cursor will not move again until a D-pad button has been held for BUTTON_REPEAT_TIME microseconds + bool firstMove; + + // So we don't miss a button press that happens between frames + uint16_t unhandledButtons; + + // The time a D-pad button has been held down for, in microseconds + int64_t btnHoldTime; + + // The number of canvas pixels to move the cursor this frame + int8_t moveX, moveY; + + // The index of the currently selected color, while SELECT is held or in EDIT_PALETTE mode + uint8_t paletteSelect; + + // Pointer to the selected color channel to edit (R, G, or B) + uint8_t* editPaletteCur; + + // The separate values for the color channels + uint8_t editPaletteR, editPaletteG, editPaletteB; + + // The color selected + paletteColor_t newColor; + + // Used for timing blinks + int64_t blinkTimer; + bool blinkOn; + + bool touchDown; + int32_t firstTouch; + int32_t lastTouch; + + // The brush width + uint8_t startBrushWidth; + + //////// Save data flags + + // True if the canvas has been modified since last save + bool unsaved; + + // Whether to perform a save or load on the next loop + bool doSave, doLoad; + + // True when a save has been started but not yet completed. Prevents input while saving. + bool saveInProgress; + + //// Save Menu Flags + + // The current state of the save / load menu + paintSaveMenu_t saveMenu; + + // The save slot selected for PICK_SLOT_SAVE and PICK_SLOT_LOAD + uint8_t selectedSlot; + + // State for Yes/No options in the save menu. + bool saveMenuBoolOption; + + //////// Rendering flags + + // If set, the canvas will be cleared and the screen will be redrawn. Set on startup. + bool clearScreen; + + // Set to redraw the toolbar on the next loop, when a brush or color is being selected + bool redrawToolbar; + + // Whether all pick points should be redrawn with the current fgColor, for when the color changes while we're + // picking + // TODO: This might not be necessary any more since we redraw those constantly. + bool recolorPickPoints; + + //////// Undo Data + + // The linked list of undo data + list_t undoList; + + // After an undo is performed, this points to the action that was undone. + // This allows redo to work. If the image is edited, this and all following items are removed. + node_t* undoHead; + + /// @brief Canvas stored so we can draw over it + paintUndo_t* storedCanvas; + + //////// Tool Wheel + + bool showToolWheel; + + // Tool wheel shown even though touch released + bool toolWheelWaiting; + + // The menu for the tool wheel + menu_t* toolWheel; + + // So we can update the brush size item options easily + menuItem_t* toolWheelBrushSizeItem; + + // A box to center the selected tool wheel item label at + rectangle_t toolWheelLabelBox; + + // So we can update the color item options easily + menuItem_t* toolWheelColorItem; + + // Labels for each color, so there's something to display on the tool wheel + // 8 chars is enough for #00AABB\0 + char colorNames[PAINT_MAX_COLORS][9]; + + // The renderer for the tool wheel menu + wheelMenuRenderer_t* toolWheelRenderer; + + //// Icons for various tool wheel things + + wsg_t wheelSizeWsg; + wsg_t wheelBrushWsg; + wsg_t wheelColorWsg; + wsg_t wheelSettingsWsg; + wsg_t wheelUndoWsg; + wsg_t wheelRedoWsg; + wsg_t wheelSaveWsg; +} paintDraw_t; + +typedef struct +{ + paintCanvas_t canvas; + int32_t index; + + font_t toolbarFont; + wsg_t arrowWsg; + + // The save slot being displayed / shared + uint8_t shareSaveSlot; + + paintShareState_t shareState; + + bool shareAcked; + bool connectionStarted; + + // For the sender, the sequence number of the current packet being sent / waiting for ack + uint16_t shareSeqNum; + + uint8_t sharePacket[P2P_MAX_DATA_LEN]; + uint8_t sharePacketLen; + + uint8_t sharePaletteMap[256]; + + // Set to true when a new packet has been written to sharePacket, either to be sent or to be handled + bool shareNewPacket; + + // Flag for updating the screen + bool shareUpdateScreen; + + // TODO rename this so it's not the same as the global one + p2pInfo p2pInfo; + + // Time for the progress bar timer + int64_t shareTime; + int64_t timeSincePacket; + + // True if we are the sender, false if not + bool isSender; + + // True if the screen should be cleared on the next loop + bool clearScreen; +} paintShare_t; + +typedef struct +{ + paintCanvas_t canvas; + int32_t index; + + font_t infoFont; + wsg_t arrow; + + // TODO rename these to better things now that they're in their own struct + + // Last timestamp of gallery transition + int64_t galleryTime; + + // Amount of time between each transition, or 0 for disabled + int64_t gallerySpeed; + int32_t gallerySpeedIndex; + + // portableDance_t* portableDances; + + // Reaining time that info text will be shown + int64_t infoTimeRemaining; + + // Current image used in gallery + uint8_t gallerySlot; + + bool showUi; + bool galleryLoadNew; + bool screensaverMode; + paintScreen_t returnScreen; + + uint8_t galleryScale; +} paintGallery_t; + +typedef struct +{ + //////// General app data + + // Main Menu Font + font_t menuFont; + // Main Menu + menu_t* menu; + menuLogbookRenderer_t* menuRenderer; + + uint8_t eraseSlot; + + bool eraseDataSelected, eraseDataConfirm; + + int64_t idleTimer; + bool enableScreensaver; + + // The screen within paint that the user is in + paintScreen_t screen; +} paintMainMenu_t; + +extern paintMainMenu_t* paintMenu; + +#endif diff --git a/main/modes/mfpaint/paint_draw.c b/main/modes/mfpaint/paint_draw.c new file mode 100644 index 000000000..8e0957347 --- /dev/null +++ b/main/modes/mfpaint/paint_draw.c @@ -0,0 +1,2618 @@ +#include "paint_draw.h" + +#include +#include "esp_heap_caps.h" + +#include "hdw-bzr.h" +#include "hdw-btn.h" +#include "touchUtils.h" + +#include "paint_ui.h" +#include "paint_brush.h" +#include "paint_nvs.h" +#include "paint_util.h" +#include "mode_paint.h" +#include "paint_song.h" +#include "paint_help.h" + +#include "wheel_menu.h" + +#include "macros.h" + +static void paintToolWheelCb(const char* label, bool selected, uint32_t settingVal); +static void paintSetupColorWheel(void); + +paintDraw_t* paintState; +paintHelp_t* paintHelp; + +static const char toolWheelTitleStr[] = "Tool Wheel"; +static const char toolWheelBrushStr[] = "Brush"; +static const char toolWheelColorStr[] = "Color"; +static const char toolWheelSizeStr[] = "Brush Size"; +static const char toolWheelOptionsStr[] = "More"; +static const char toolWheelUndoStr[] = "Undo"; +static const char toolWheelRedoStr[] = "Redo"; + +static const char toolWheelSaveStr[] = "Save"; +static const char toolWheelLoadStr[] = "Load"; +static const char toolWheelNewStr[] = "New"; +static const char toolWheelExitStr[] = "Stop Drawing"; + +static paletteColor_t defaultPalette[] = { + c000, // black + c555, // white + c012, // dark blue + c505, // fuchsia + c540, // yellow + c235, // cornflower + + c222, // light gray + c444, // dark gray + + c500, // red + c050, // green + c055, // cyan + c005, // blue + c530, // orange? + c503, // pink + c350, // lime green + c522, // salmon +}; + +brush_t brushes[] = { + {.name = "Square Pen", + .mode = HOLD_DRAW, + .maxPoints = 1, + .minSize = 1, + .maxSize = 16, + .fnDraw = paintDrawSquarePen, + .iconName = "square_pen"}, + {.name = "Circle Pen", + .mode = HOLD_DRAW, + .maxPoints = 1, + .minSize = 1, + .maxSize = 16, + .fnDraw = paintDrawCirclePen, + .iconName = "circle_pen"}, + {.name = "Line", + .mode = PICK_POINT, + .maxPoints = 2, + .minSize = 1, + .maxSize = 8, + .fnDraw = paintDrawLine, + .iconName = "line"}, + {.name = "Bezier Curve", + .mode = PICK_POINT, + .maxPoints = 4, + .minSize = 1, + .maxSize = 8, + .fnDraw = paintDrawCurve, + .iconName = "curve"}, + {.name = "Rectangle", + .mode = PICK_POINT, + .maxPoints = 2, + .minSize = 1, + .maxSize = 8, + .fnDraw = paintDrawRectangle, + .iconName = "rect"}, + {.name = "Filled Rectangle", + .mode = PICK_POINT, + .maxPoints = 2, + .minSize = 0, + .maxSize = 0, + .fnDraw = paintDrawFilledRectangle, + .iconName = "rect_filled"}, + {.name = "Circle", + .mode = PICK_POINT, + .maxPoints = 2, + .minSize = 1, + .maxSize = 8, + .fnDraw = paintDrawCircle, + .iconName = "circle"}, + {.name = "Filled Circle", + .mode = PICK_POINT, + .maxPoints = 2, + .minSize = 0, + .maxSize = 0, + .fnDraw = paintDrawFilledCircle, + .iconName = "circle_filled"}, + {.name = "Ellipse", + .mode = PICK_POINT, + .maxPoints = 2, + .minSize = 1, + .maxSize = 8, + .fnDraw = paintDrawEllipse, + .iconName = "ellipse"}, + {.name = "Polygon", + .mode = PICK_POINT_LOOP, + .maxPoints = 16, + .minSize = 1, + .maxSize = 8, + .fnDraw = paintDrawPolygon, + .iconName = "polygon"}, + {.name = "Squarewave", + .mode = PICK_POINT, + .maxPoints = 2, + .minSize = 0, + .maxSize = 0, + .fnDraw = paintDrawSquareWave, + .iconName = "squarewave"}, + {.name = "Paint Bucket", + .mode = PICK_POINT, + .maxPoints = 1, + .minSize = 0, + .maxSize = 0, + .fnDraw = paintDrawPaintBucket, + .iconName = "paint_bucket"}, +}; + +const char activeIconStr[] = "%s_active.wsg"; +const char inactiveIconStr[] = "%s_inactive.wsg"; + +const brush_t* firstBrush = brushes; +const brush_t* lastBrush = brushes + sizeof(brushes) / sizeof(brushes[0]) - 1; + +static void paintSetupColorWheel(void) +{ + for (uint8_t i = 0; i < PAINT_MAX_COLORS; ++i) + { + char* colorLabel = paintState->colorNames[i]; + snprintf(colorLabel, sizeof(paintState->colorNames[0]), "#%02X%02X%02X", + (paintState->canvas.palette[i] / 36) * 51, ((paintState->canvas.palette[i] / 6) % 6) * 51, + (paintState->canvas.palette[i] % 6) * 51); + wheelMenuSetItemInfo(paintState->toolWheelRenderer, colorLabel, NULL, i, NO_SCROLL); + wheelMenuSetItemColor(paintState->toolWheelRenderer, colorLabel, paintState->canvas.palette[i], + paintState->canvas.palette[i]); + } +} + +void paintDrawScreenSetup(void) +{ + PAINT_LOGD("Allocating %" PRIu32 " bytes for paintState", (uint32_t)sizeof(paintDraw_t)); + paintState = calloc(sizeof(paintDraw_t), 1); + + loadFont(PAINT_TOOLBAR_FONT, &(paintState->toolbarFont), false); + loadFont(PAINT_SAVE_MENU_FONT, &(paintState->saveMenuFont), false); + loadFont(PAINT_SMALL_FONT, &(paintState->smallFont), false); + paintState->clearScreen = true; + paintState->blinkOn = true; + paintState->blinkTimer = 0; + + // Set up the brush icons + uint16_t spriteH = 0; + char iconName[32]; + for (brush_t* brush = brushes; brush <= lastBrush; brush++) + { + snprintf(iconName, sizeof(iconName), activeIconStr, brush->iconName); + if (!loadWsg(iconName, &brush->iconActive, false)) + { + PAINT_LOGE("Loading icon %s failed!!!", iconName); + } + + snprintf(iconName, sizeof(iconName), inactiveIconStr, brush->iconName); + if (!loadWsg(iconName, &brush->iconInactive, false)) + { + PAINT_LOGE("Loading icon %s failed!!!", iconName); + } + + // Keep track of the tallest sprite for layout purposes + if (brush->iconActive.h > spriteH) + { + spriteH = brush->iconActive.h; + } + + if (brush->iconInactive.h > spriteH) + { + spriteH = brush->iconInactive.h; + } + } + + if (!loadWsg("pointer.wsg", &paintState->picksWsg, false)) + { + PAINT_LOGE("Loading pointer.wsg icon failed!!!"); + } + + if (!loadWsg("brush_size.wsg", &paintState->brushSizeWsg, false)) + { + PAINT_LOGE("Loading brush_size.wsg icon failed!!!"); + } + + if (!loadWsg("arrow9.wsg", &paintState->smallArrowWsg, false)) + { + PAINT_LOGE("Loading arrow5.wsg icon failed!!!"); + } + else + { + colorReplaceWsg(&paintState->smallArrowWsg, c555, c000); + } + + if (!loadWsg("arrow12.wsg", &paintState->bigArrowWsg, false)) + { + PAINT_LOGE("Loading arrow5.wsg icon failed!!!"); + } + else + { + colorReplaceWsg(&paintState->bigArrowWsg, c555, c000); + } + + if (!loadWsg("newfile.wsg", &paintState->newfileWsg, false)) + { + PAINT_LOGE("Loading newfile.wsg icon failed!!!"); + } + + if (!loadWsg("overwrite.wsg", &paintState->overwriteWsg, false)) + { + PAINT_LOGE("Loading overwrite.wsg icon failed!!!"); + } + + paintState->marginTop = TFT_CORNER_RADIUS * 2 / 3; + + // Left: Leave room for the color boxes, their margins, their borders, and the canvas border + paintState->marginLeft = TFT_CORNER_RADIUS * 2 / 3; + // Bottom: Leave room for the brush name/icon/color boxes, 4px margin, and the canvas border + paintState->marginBottom + = MAX(brushes->iconActive.h + 1, MAX(paintState->toolbarFont.height + PAINT_COLORBOX_MARGIN_X + 1, + PAINT_COLORBOX_H + PAINT_COLORBOX_H / 2 + 2 + PAINT_COLORBOX_MARGIN_X)); + // Right: We just need to stay away from the rounded corner, so like, 12px? + paintState->marginRight = TFT_CORNER_RADIUS * 2 / 3; + + if (paintHelp != NULL) + { + // Set up some tutorial things that depend on basic paintState data + paintTutorialPostSetup(); + + // We're in help mode! We need some more space for the text + paintState->marginBottom += paintHelp->helpH; + } + + paintLoadIndex(&paintState->index); + + if (paintHelp == NULL && paintGetAnySlotInUse(paintState->index) + && paintGetRecentSlot(paintState->index) != PAINT_SAVE_SLOTS) + { + // If there's a saved image, load that (but not in the tutorial) + paintState->selectedSlot = paintGetRecentSlot(paintState->index); + paintState->doLoad = true; + } + else + { + // Set up a blank canvas with the default size + paintState->canvas.w = PAINT_DEFAULT_CANVAS_WIDTH; + paintState->canvas.h = PAINT_DEFAULT_CANVAS_HEIGHT; + + // Automatically position the canvas in the center of the drawable area at the max scale that will fit + paintPositionDrawCanvas(); + + // load the default palette + memcpy(paintState->canvas.palette, defaultPalette, PAINT_MAX_COLORS * sizeof(paletteColor_t)); + getArtist()->fgColor = paintState->canvas.palette[0]; + getArtist()->bgColor = paintState->canvas.palette[1]; + } + + // This assumes the first brush is a pen brush, which it always will be unless we rearrange the brush array + paintGenerateCursorSprite(&paintState->cursorWsg, &paintState->canvas, firstBrush->minSize); + + // Init the cursors for each artist + // TODO only do one for singleplayer? + for (uint8_t i = 0; i < sizeof(paintState->artist) / sizeof(paintState->artist[0]); i++) + { + initCursor(&paintState->artist[i].cursor, &paintState->canvas, &paintState->cursorWsg); + initPxStack(&paintState->artist[i].pickPoints); + paintState->artist[i].brushDef = firstBrush; + paintState->artist[i].brushWidth = firstBrush->minSize; + + setCursorSprite(&paintState->artist[i].cursor, &paintState->canvas, &paintState->cursorWsg); + setCursorOffset(&paintState->artist[i].cursor, (paintState->canvas.xScale - paintState->cursorWsg.w) / 2, + (paintState->canvas.yScale - paintState->cursorWsg.h) / 2); + moveCursorAbsolute(getCursor(), &paintState->canvas, paintState->canvas.w / 2, paintState->canvas.h / 2); + } + + clearPxTft(); + + paintSetupTool(); + + // Clear the LEDs + // Might not be necessary here + paintUpdateLeds(); + + bzrStop(); + bzrPlayBgm(&paintBgm, BZR_LEFT); + + // Set up the tool wheel + paintState->showToolWheel = false; + paintState->toolWheelWaiting = false; + paintState->toolWheel = initMenu(toolWheelTitleStr, paintToolWheelCb); + paintState->toolWheelRenderer = initWheelMenu(&paintState->toolbarFont, 90, &paintState->toolWheelLabelBox); + + paintState->toolWheelLabelBox.x = TFT_CORNER_RADIUS; + paintState->toolWheelLabelBox.y = 4; + paintState->toolWheelLabelBox.width = TFT_WIDTH - TFT_CORNER_RADIUS * 2; + paintState->toolWheelLabelBox.height = 20; + + // Tool wheel icons + if (!loadWsg("wheel_brush.wsg", &paintState->wheelBrushWsg, false)) + { + PAINT_LOGE("Loading wheel_brush.wsg icon failed!!!"); + } + + if (!loadWsg("wheel_color.wsg", &paintState->wheelColorWsg, false)) + { + PAINT_LOGE("Loading wheel_color.wsg icon failed!!!"); + } + + if (!loadWsg("wheel_size.wsg", &paintState->wheelSizeWsg, false)) + { + PAINT_LOGE("Loading wheel_size.wsg icon failed!!!"); + } + + if (!loadWsg("wheel_options.wsg", &paintState->wheelSettingsWsg, false)) + { + PAINT_LOGE("Loading wheel_options.wsg icon failed!!!"); + } + + if (!loadWsg("wheel_undo.wsg", &paintState->wheelUndoWsg, false)) + { + PAINT_LOGE("Loading wheel_undo.wsg icon failed!!!"); + } + + if (!loadWsg("wheel_redo.wsg", &paintState->wheelRedoWsg, false)) + { + PAINT_LOGE("Loading wheel_redo.wsg icon failed!!!"); + } + + if (!loadWsg("wheel_save.wsg", &paintState->wheelSaveWsg, false)) + { + PAINT_LOGE("Loading wheel_save.wsg icon failed!!!"); + } + + // Top: Sub-menu for Brush + paintState->toolWheel = startSubMenu(paintState->toolWheel, toolWheelBrushStr); + wheelMenuSetItemInfo(paintState->toolWheelRenderer, toolWheelBrushStr, &paintState->wheelBrushWsg, 0, SCROLL_HORIZ); + + for (const brush_t* brush = brushes; brush <= lastBrush; brush++) + { + addSingleItemToMenu(paintState->toolWheel, brush->name); + wheelMenuSetItemInfo(paintState->toolWheelRenderer, brush->name, &brush->iconInactive, brush - brushes, + NO_SCROLL); + } + + paintState->toolWheel = endSubMenu(paintState->toolWheel); + + // Left: Sub-menu for Color + paintState->toolWheel = startSubMenu(paintState->toolWheel, toolWheelColorStr); + wheelMenuSetItemInfo(paintState->toolWheelRenderer, toolWheelColorStr, &paintState->wheelColorWsg, 1, + SCROLL_VERT_R); + + // Add the actual menu items (only once) + for (uint8_t i = 0; i < PAINT_MAX_COLORS; ++i) + { + addSingleItemToMenu(paintState->toolWheel, paintState->colorNames[i]); + } + + // Set up the infos for the color wheel + paintSetupColorWheel(); + + paintState->toolWheel = endSubMenu(paintState->toolWheel); + + // Bottom-left: Undo + addSingleItemToMenu(paintState->toolWheel, toolWheelUndoStr); + wheelMenuSetItemInfo(paintState->toolWheelRenderer, toolWheelUndoStr, &paintState->wheelUndoWsg, 2, NO_SCROLL); + + // Bottom-right: Redo + addSingleItemToMenu(paintState->toolWheel, toolWheelRedoStr); + wheelMenuSetItemInfo(paintState->toolWheelRenderer, toolWheelRedoStr, &paintState->wheelRedoWsg, 4, NO_SCROLL); + + // Bottom: Options sub-menu + paintState->toolWheel = startSubMenu(paintState->toolWheel, toolWheelOptionsStr); + wheelMenuSetItemInfo(paintState->toolWheelRenderer, toolWheelOptionsStr, &paintState->wheelSettingsWsg, 3, + NO_SCROLL); + + // Options menu + + // Top: Load + addSingleItemToMenu(paintState->toolWheel, toolWheelLoadStr); + wheelMenuSetItemInfo(paintState->toolWheelRenderer, toolWheelLoadStr, &brushes[4].iconInactive, 0, NO_SCROLL); + + // Left: New + addSingleItemToMenu(paintState->toolWheel, toolWheelNewStr); + wheelMenuSetItemInfo(paintState->toolWheelRenderer, toolWheelNewStr, &paintState->newfileWsg, 1, NO_SCROLL); + + // Bottom: Exit/Quit + addSingleItemToMenu(paintState->toolWheel, toolWheelExitStr); + wheelMenuSetItemInfo(paintState->toolWheelRenderer, toolWheelExitStr, &paintState->overwriteWsg, 2, NO_SCROLL); + + // Right: Save + addSingleItemToMenu(paintState->toolWheel, toolWheelSaveStr); + wheelMenuSetItemInfo(paintState->toolWheelRenderer, toolWheelSaveStr, &paintState->wheelSaveWsg, 3, NO_SCROLL); + + paintState->toolWheel = endSubMenu(paintState->toolWheel); + + // Right: Up/Down for Size + settingParam_t sizeBounds = { + .min = getArtist()->brushDef->minSize, + .max = getArtist()->brushDef->maxSize, + .def = getArtist()->brushWidth, + .key = NULL, + }; + addSettingsItemToMenu(paintState->toolWheel, toolWheelSizeStr, &sizeBounds, sizeBounds.def); + paintState->toolWheelBrushSizeItem = paintState->toolWheel->items->last->val; + wheelMenuSetItemInfo(paintState->toolWheelRenderer, toolWheelSizeStr, &paintState->wheelSizeWsg, 5, SCROLL_VERT); + + // PAINT_LOGI("It's paintin' time! Canvas is %" PRIu16 " x %" PRIu16 " pixels!", paintState->canvas.w, + // paintState->canvas.h); +} + +void paintDrawScreenCleanup(void) +{ + bzrStop(); + + deinitWheelMenu(paintState->toolWheelRenderer); + deinitMenu(paintState->toolWheel); + + freeWsg(&paintState->wheelColorWsg); + freeWsg(&paintState->wheelBrushWsg); + freeWsg(&paintState->wheelSizeWsg); + freeWsg(&paintState->wheelSettingsWsg); + freeWsg(&paintState->wheelUndoWsg); + freeWsg(&paintState->wheelRedoWsg); + freeWsg(&paintState->wheelSaveWsg); + + for (brush_t* brush = brushes; brush <= lastBrush; brush++) + { + freeWsg(&brush->iconActive); + freeWsg(&brush->iconInactive); + } + + freeWsg(&paintState->brushSizeWsg); + freeWsg(&paintState->picksWsg); + freeWsg(&paintState->bigArrowWsg); + freeWsg(&paintState->smallArrowWsg); + freeWsg(&paintState->newfileWsg); + freeWsg(&paintState->overwriteWsg); + + for (uint8_t i = 0; i < sizeof(paintState->artist) / sizeof(paintState->artist[0]); i++) + { + deinitCursor(&paintState->artist[i].cursor); + freePxStack(&paintState->artist[i].pickPoints); + } + + if (paintState->storedCanvas) + { + free(paintState->storedCanvas); + } + + paintFreeCursorSprite(&paintState->cursorWsg); + paintFreeUndos(); + + freeFont(&paintState->smallFont); + freeFont(&paintState->saveMenuFont); + freeFont(&paintState->toolbarFont); + free(paintState); +} + +void paintTutorialSetup(void) +{ + paintHelp = calloc(sizeof(paintHelp_t), 1); + paintHelp->curHelp = helpSteps; +} + +// gets called after paintState is allocated and has basic info, but before canvas layout is done +void paintTutorialPostSetup(void) +{ + paintHelp->helpH = PAINT_HELP_TEXT_LINES * (paintState->toolbarFont.height + 1) - 1; +} + +void paintTutorialCleanup(void) +{ + free(paintHelp); + paintHelp = NULL; +} + +void paintTutorialOnEvent(void) +{ + if (paintTutorialCheckTrigger(&paintHelp->curHelp->trigger)) + { + paintState->redrawToolbar = true; + if (paintHelp->curHelp != lastHelp) + { + paintHelp->curHelp++; + paintHelp->allButtons = 0; + paintHelp->lastButton = 0; + paintHelp->lastButtonDown = false; + paintHelp->drawComplete = false; + } + } + else if (paintTutorialCheckTrigger(&paintHelp->curHelp->backtrack)) + { + paintState->redrawToolbar = true; + + // check some bonuds even though it's constant + if (paintHelp->curHelp - paintHelp->curHelp->backtrackSteps >= helpSteps) + { + paintHelp->curHelp -= paintHelp->curHelp->backtrackSteps; + } + else + { + paintHelp->curHelp = helpSteps; + } + + paintHelp->allButtons = 0; + paintHelp->lastButton = 0; + paintHelp->lastButtonDown = false; + paintHelp->drawComplete = false; + } +} + +bool paintTutorialCheckTrigger(const paintHelpTrigger_t* trigger) +{ + switch (trigger->type) + { + case PRESS_ALL: + return (paintHelp->allButtons & trigger->data) == trigger->data; + + case PRESS_ANY: + return (paintHelp->curButtons & trigger->data) != 0 && paintHelp->lastButtonDown; + + case PRESS: + return (paintHelp->curButtons & (trigger->data)) == trigger->data && paintHelp->lastButtonDown; + + case RELEASE: + return paintHelp->lastButtonDown == false + && (paintHelp->lastButton & trigger->data) == paintHelp->lastButton; + + case DRAW_COMPLETE: + return paintHelp->drawComplete; + + case CHANGE_BRUSH: + return !strcmp(getArtist()->brushDef->name, trigger->dataPtr) && paintHelp->curButtons == 0; + + case CHANGE_MODE: + return paintState->buttonMode == trigger->data; + + case BRUSH_NOT: + return strcmp(getArtist()->brushDef->name, trigger->dataPtr) && paintHelp->curButtons == 0; + + case SELECT_MENU_ITEM: + return paintState->saveMenu == trigger->data; + + case MENU_ITEM_NOT: + return paintState->saveMenu != trigger->data; + + case MODE_NOT: + return paintState->buttonMode != trigger->data; + + case NO_TRIGGER: + default: + break; + } + + return false; +} + +void paintPositionDrawCanvas(void) +{ + // Calculate the highest scale that will fit on the screen + uint8_t scale + = paintGetMaxScale(paintState->canvas.w, paintState->canvas.h, paintState->marginLeft + paintState->marginRight, + paintState->marginTop + paintState->marginBottom); + + paintState->canvas.xScale = scale; + paintState->canvas.yScale = scale; + + paintState->canvas.x = paintState->marginLeft + + (TFT_WIDTH - paintState->marginLeft - paintState->marginRight + - paintState->canvas.w * paintState->canvas.xScale) + / 2; + paintState->canvas.y = paintState->marginTop + + (TFT_HEIGHT - paintState->marginTop - paintState->marginBottom + - paintState->canvas.h * paintState->canvas.yScale) + / 2; +} + +void paintDrawScreenMainLoop(int64_t elapsedUs) +{ + paintDrawScreenPollTouch(); + + // Screen Reset + if (paintState->clearScreen) + { + hideCursor(getCursor(), &paintState->canvas); + memcpy(paintState->canvas.palette, defaultPalette, PAINT_MAX_COLORS * sizeof(paletteColor_t)); + getArtist()->fgColor = paintState->canvas.palette[0]; + getArtist()->bgColor = paintState->canvas.palette[1]; + paintClearCanvas(&paintState->canvas, getArtist()->bgColor); + paintRenderToolbar(getArtist(), &paintState->canvas, paintState, firstBrush, lastBrush); + paintUpdateLeds(); + showCursor(getCursor(), &paintState->canvas); + paintState->unsaved = false; + paintState->clearScreen = false; + } + + // Save and Load + if (paintState->doSave || paintState->doLoad) + { + paintState->saveInProgress = true; + + if (paintState->doSave) + { + hideCursor(getCursor(), &paintState->canvas); + paintHidePickPoints(); + paintSave(&paintState->index, &paintState->canvas, paintState->selectedSlot); + paintDrawPickPoints(); + showCursor(getCursor(), &paintState->canvas); + } + else + { + if (paintGetSlotInUse(paintState->index, paintState->selectedSlot)) + { + // Load from the selected slot if it's been used + hideCursor(getCursor(), &paintState->canvas); + paintClearCanvas(&paintState->canvas, getArtist()->bgColor); + if (paintLoadDimensions(&paintState->canvas, paintState->selectedSlot)) + { + paintPositionDrawCanvas(); + paintLoad(&paintState->index, &paintState->canvas, paintState->selectedSlot); + paintSetRecentSlot(&paintState->index, paintState->selectedSlot); + + paintFreeUndos(); + + getArtist()->fgColor = paintState->canvas.palette[0]; + getArtist()->bgColor = paintState->canvas.palette[1]; + + // Do the tool setup, which will also setup the cursor + paintSetupTool(); + + // Put the cursor in the middle of the screen + moveCursorAbsolute(getCursor(), &paintState->canvas, paintState->canvas.w / 2, + paintState->canvas.h / 2); + showCursor(getCursor(), &paintState->canvas); + paintUpdateLeds(); + } + else + { + PAINT_LOGE("Slot %" PRIu8 " has 0 dimension! Stopping load and clearing slot", + paintState->selectedSlot); + paintClearSlot(&paintState->index, paintState->selectedSlot); + paintReturnToMainMenu(); + } + } + else + { + // If the slot hasn't been used yet, just clear the screen + paintState->clearScreen = true; + } + } + + paintState->unsaved = false; + paintState->doSave = false; + paintState->doLoad = false; + paintState->saveInProgress = false; + + paintState->buttonMode = BTN_MODE_DRAW; + paintState->saveMenu = HIDDEN; + + paintState->redrawToolbar = true; + } + + if (paintState->recolorPickPoints) + { + paintDrawPickPoints(); + paintUpdateLeds(); + paintState->recolorPickPoints = false; + } + + // TODO render toolbar always + // paintRenderToolbar(getArtist(), &paintState->canvas, paintState, firstBrush, lastBrush); + + if (wheelMenuActive(paintState->toolWheel, paintState->toolWheelRenderer)) + { + paintEnterSelectMode(); + + paintClearCanvas(&paintState->canvas, PAINT_TOOLBAR_BG); + paintRenderToolbar(getArtist(), &paintState->canvas, paintState, firstBrush, lastBrush); + drawWheelMenu(paintState->toolWheel, paintState->toolWheelRenderer, elapsedUs); + } + else + { + paintExitSelectMode(); + paintRenderToolbar(getArtist(), &paintState->canvas, paintState, firstBrush, lastBrush); + // Don't remember why we only do this when redrawToolbar is true + // Oh, it's because `paintState->redrawToolbar` is mostly only set in select mode unless you press B? + if (paintState->aHeld || paintState->aPress) + { + paintDoTool(getCursor()->x, getCursor()->y, getArtist()->fgColor); + + if (getArtist()->brushDef->mode != HOLD_DRAW) + { + paintState->aHeld = false; + } + + paintState->aPress = false; + } + + if (paintState->moveX || paintState->moveY || paintState->unhandledButtons) + { + bool clearMovement = false; + + if (!paintState->moveX && !paintState->moveY) + { + paintHandleDpad(paintState->unhandledButtons); + clearMovement = true; + } + + paintState->btnHoldTime += elapsedUs; + if (paintState->firstMove || paintState->btnHoldTime >= BUTTON_REPEAT_TIME) + { + moveCursorRelative(getCursor(), &paintState->canvas, paintState->moveX, paintState->moveY); + paintRenderToolbar(getArtist(), &paintState->canvas, paintState, firstBrush, lastBrush); + + paintState->firstMove = false; + } + + paintState->unhandledButtons = 0; + if (clearMovement) + { + paintState->moveX = 0; + paintState->moveY = 0; + } + } + + if (paintState->index & PAINT_ENABLE_BLINK) + { + if (paintState->blinkOn && paintState->blinkTimer >= BLINK_TIME_ON) + { + paintState->blinkTimer %= BLINK_TIME_ON; + paintState->blinkOn = false; + paintHidePickPoints(); + } + else if (!paintState->blinkOn && paintState->blinkTimer >= BLINK_TIME_OFF) + { + paintState->blinkTimer %= BLINK_TIME_OFF; + paintState->blinkOn = true; + paintDrawPickPoints(); + } + else if (paintState->blinkOn) + { + paintDrawPickPoints(); + } + + paintState->blinkTimer += elapsedUs; + } + else + { + paintDrawPickPoints(); + } + + drawCursor(getCursor(), &paintState->canvas); + } + + if (paintHelp != NULL) + { + int16_t pastColorBoxX = PAINT_COLORBOX_MARGIN_X + + (paintState->canvas.x - 1 - PAINT_COLORBOX_W - PAINT_COLORBOX_MARGIN_X * 2 - 2) / 2 + + PAINT_COLORBOX_W + 2 + 6; + int16_t wrapY = paintState->canvas.y + paintState->canvas.h * paintState->canvas.yScale + 3; + const char* rest + = drawTextWordWrap(&paintState->toolbarFont, c000, paintHelp->curHelp->prompt, &pastColorBoxX, &wrapY, + TFT_WIDTH, TFT_HEIGHT - paintState->marginBottom + paintHelp->helpH); + if (rest) + { + PAINT_LOGW("Some tutorial text didn't fit: %s", rest); + } + } +} + +void paintSaveModePrevItem(void) +{ + switch (paintState->saveMenu) + { + case HIDDEN: + break; + + case UNDO: + paintState->saveMenu = EXIT; + break; + + case REDO: + paintState->saveMenu = UNDO; + break; + + case PICK_SLOT_SAVE: + case CONFIRM_OVERWRITE: + paintState->saveMenu = REDO; + break; + + case PICK_SLOT_LOAD: + case CONFIRM_UNSAVED: + paintState->saveMenu = PICK_SLOT_SAVE; + break; + + case EDIT_PALETTE: + case COLOR_PICKER: + paintState->saveMenu = PICK_SLOT_LOAD; + break; + + case CLEAR: + case CONFIRM_CLEAR: + paintState->saveMenu = EDIT_PALETTE; + break; + + case EXIT: + case CONFIRM_EXIT: + paintState->saveMenu = CLEAR; + break; + } + + paintState->saveMenuBoolOption = false; + + // Check to make sure we can actually redo + if (paintState->saveMenu == REDO && !paintCanRedo()) + { + // Nothing to redo, go to next + paintState->saveMenu = UNDO; + } + + // Check to make sure we can actually undo + if (paintState->saveMenu == UNDO && !paintCanUndo()) + { + paintState->saveMenu = EXIT; + } + + // If we're selecting "Load", then make sure we can actually load a slot + if (paintState->saveMenu == PICK_SLOT_LOAD) + { + // If no slots are in use, skip again + if (!paintGetAnySlotInUse(paintState->index)) + { + paintState->saveMenu = PICK_SLOT_SAVE; + } + else if (!paintGetSlotInUse(paintState->index, paintState->selectedSlot)) + { + // Otherwise, make sure the selected slot is in use + paintState->selectedSlot = paintGetNextSlotInUse(paintState->index, paintState->selectedSlot); + } + } +} + +void paintSaveModeNextItem(void) +{ + switch (paintState->saveMenu) + { + case HIDDEN: + break; + + case UNDO: + paintState->saveMenu = REDO; + break; + + case REDO: + paintState->saveMenu = PICK_SLOT_SAVE; + break; + + case PICK_SLOT_SAVE: + case CONFIRM_OVERWRITE: + paintState->saveMenu = PICK_SLOT_LOAD; + break; + + case PICK_SLOT_LOAD: + case CONFIRM_UNSAVED: + paintState->saveMenu = EDIT_PALETTE; + break; + + case EDIT_PALETTE: + case COLOR_PICKER: + paintState->saveMenu = CLEAR; + break; + + case CLEAR: + case CONFIRM_CLEAR: + paintState->saveMenu = EXIT; + break; + + case EXIT: + case CONFIRM_EXIT: + paintState->saveMenu = UNDO; + break; + } + + paintState->saveMenuBoolOption = false; + + // Check to make sure we can actually undo + if (paintState->saveMenu == UNDO && !paintCanUndo()) + { + paintState->saveMenu = REDO; + } + + // Check to make sure we can actually redo + if (paintState->saveMenu == REDO && !paintCanRedo()) + { + // Nothing to redo, go to next + paintState->saveMenu = PICK_SLOT_SAVE; + } + + // If we're selecting "Load", then make sure we can actually load a slot + if (paintState->saveMenu == PICK_SLOT_LOAD) + { + // If no slots are in use, skip again + if (!paintGetAnySlotInUse(paintState->index)) + { + paintState->saveMenu = EDIT_PALETTE; + } + else if (!paintGetSlotInUse(paintState->index, paintState->selectedSlot)) + { + // Otherwise, make sure the selected slot is in use + paintState->selectedSlot = paintGetNextSlotInUse(paintState->index, paintState->selectedSlot); + } + } +} + +void paintSaveModePrevOption(void) +{ + switch (paintState->saveMenu) + { + case PICK_SLOT_SAVE: + paintState->selectedSlot = PREV_WRAP(paintState->selectedSlot, PAINT_SAVE_SLOTS); + break; + + case PICK_SLOT_LOAD: + paintState->selectedSlot = paintGetPrevSlotInUse(paintState->index, paintState->selectedSlot); + break; + + case CONFIRM_OVERWRITE: + case CONFIRM_UNSAVED: + case CONFIRM_CLEAR: + case CONFIRM_EXIT: + // Just flip the state + paintState->saveMenuBoolOption = !paintState->saveMenuBoolOption; + break; + + case HIDDEN: + case UNDO: + case REDO: + case EDIT_PALETTE: + case COLOR_PICKER: + case CLEAR: + case EXIT: + // Do nothing, there are no options here + break; + } +} + +void paintSaveModeNextOption(void) +{ + switch (paintState->saveMenu) + { + case PICK_SLOT_SAVE: + paintState->selectedSlot = NEXT_WRAP(paintState->selectedSlot, PAINT_SAVE_SLOTS); + break; + + case PICK_SLOT_LOAD: + paintState->selectedSlot = paintGetNextSlotInUse(paintState->index, paintState->selectedSlot); + break; + + case CONFIRM_OVERWRITE: + case CONFIRM_UNSAVED: + case CONFIRM_CLEAR: + case CONFIRM_EXIT: + // Just flip the state + paintState->saveMenuBoolOption = !paintState->saveMenuBoolOption; + break; + + case HIDDEN: + case UNDO: + case REDO: + case EDIT_PALETTE: + case COLOR_PICKER: + case CLEAR: + case EXIT: + // Do nothing, there are no options here + break; + } +} + +void paintEditPaletteUpdate(void) +{ + paintState->newColor = (paintState->editPaletteR * 36 + paintState->editPaletteG * 6 + paintState->editPaletteB); + paintState->redrawToolbar = true; + paintUpdateLeds(); +} + +void paintEditPaletteSetChannelValue(uint8_t val) +{ + *(paintState->editPaletteCur) = val % 6; + paintEditPaletteUpdate(); +} + +// void paintEditPaletteDecChannel(void) +// { +// *(paintState->editPaletteCur) = PREV_WRAP(*(paintState->editPaletteCur), 6); +// paintEditPaletteUpdate(); +// } + +void paintEditPaletteIncChannel(void) +{ + *(paintState->editPaletteCur) = NEXT_WRAP(*(paintState->editPaletteCur), 6); + paintEditPaletteUpdate(); +} + +void paintEditPaletteNextChannel(void) +{ + if (paintState->editPaletteCur == &paintState->editPaletteR) + { + paintState->editPaletteCur = &paintState->editPaletteG; + } + else if (paintState->editPaletteCur == &paintState->editPaletteG) + { + paintState->editPaletteCur = &paintState->editPaletteB; + } + else + { + paintState->editPaletteCur = &paintState->editPaletteR; + } +} + +void paintEditPalettePrevChannel(void) +{ + if (paintState->editPaletteCur == &paintState->editPaletteR) + { + paintState->editPaletteCur = &paintState->editPaletteB; + } + else if (paintState->editPaletteCur == &paintState->editPaletteG) + { + paintState->editPaletteCur = &paintState->editPaletteR; + } + else + { + paintState->editPaletteCur = &paintState->editPaletteG; + } +} + +void paintEditPaletteSetupColor(void) +{ + paletteColor_t col = paintState->canvas.palette[paintState->paletteSelect]; + paintState->editPaletteCur = &paintState->editPaletteR; + paintState->editPaletteR = col / 36; + paintState->editPaletteG = (col / 6) % 6; + paintState->editPaletteB = col % 6; + paintState->newColor = col; + paintEditPaletteUpdate(); +} + +void paintEditPalettePrevColor(void) +{ + paintState->paletteSelect = PREV_WRAP(paintState->paletteSelect, PAINT_MAX_COLORS); + paintEditPaletteSetupColor(); +} + +void paintEditPaletteNextColor(void) +{ + paintState->paletteSelect = NEXT_WRAP(paintState->paletteSelect, PAINT_MAX_COLORS); + paintEditPaletteSetupColor(); +} + +void paintEditPaletteConfirm(void) +{ + paintStoreUndo(&paintState->canvas); + + // Save the old color, and update the palette with the new color + paletteColor_t old = paintState->canvas.palette[paintState->paletteSelect]; + paletteColor_t new = paintState->newColor; + paintState->canvas.palette[paintState->paletteSelect] = new; + + // Only replace the color on the canvas if the old color is no longer in the palette + bool doReplace = true; + for (uint8_t i = 0; i < PAINT_MAX_COLORS; i++) + { + if (paintState->canvas.palette[i] == old) + { + doReplace = false; + break; + } + } + + // This color is no longer in the palette, so replace it with the new one + if (doReplace) + { + // Make sure the FG/BG colors aren't outsie of the palette + if (getArtist()->fgColor == old) + { + getArtist()->fgColor = new; + } + + if (getArtist()->bgColor == old) + { + getArtist()->bgColor = new; + } + + hideCursor(getCursor(), &paintState->canvas); + paintHidePickPoints(); + + // And replace it within the canvas + paintColorReplace(&paintState->canvas, old, new); + paintState->unsaved = true; + + paintDrawPickPoints(); + showCursor(getCursor(), &paintState->canvas); + } +} + +void paintPaletteModeButtonCb(const buttonEvt_t* evt) +{ + if (evt->down) + { + paintState->redrawToolbar = true; + switch (evt->button) + { + case PB_A: + { + // Don't do anything? Confirm change? + paintEditPaletteConfirm(); + break; + } + + case PB_B: + { + // Revert back to the original color + if (paintState->newColor != paintState->canvas.palette[paintState->paletteSelect]) + { + paintEditPaletteSetupColor(); + } + else + { + paintState->paletteSelect = 0; + paintState->buttonMode = BTN_MODE_DRAW; + paintState->saveMenu = HIDDEN; + } + break; + } + + case PB_START: + { + // Handled in button up + break; + } + + case PB_SELECT: + { + // {R/G/B}++ + // We will normally use the touchpad for this + paintEditPaletteIncChannel(); + break; + } + + case PB_UP: + { + // Prev color + paintEditPalettePrevColor(); + break; + } + + case PB_DOWN: + { + // Next color + paintEditPaletteNextColor(); + break; + } + + case PB_LEFT: + { + // Swap between R, G, and B + paintEditPalettePrevChannel(); + break; + } + + case PB_RIGHT: + { + // Swap between R, G, and B + paintEditPaletteNextChannel(); + break; + } + } + } + else + { + // Button up + if (evt->button == PB_START) + { + // Return to draw mode + paintState->paletteSelect = 0; + paintState->buttonMode = BTN_MODE_DRAW; + paintState->saveMenu = HIDDEN; + } + } +} + +void paintSaveModeButtonCb(const buttonEvt_t* evt) +{ + if (evt->down) + { + //////// Save menu button down + paintState->redrawToolbar = true; + switch (evt->button) + { + case PB_A: + { + switch (paintState->saveMenu) + { + case UNDO: + { + paintUndo(&paintState->canvas); + if (paintState->undoHead != NULL && paintState->undoHead->prev == NULL) + { + paintState->saveMenu = REDO; + } + break; + } + + case REDO: + { + paintRedo(&paintState->canvas); + if (paintState->undoHead != NULL && paintState->undoHead->next == NULL) + { + paintState->saveMenu = UNDO; + } + break; + } + + case PICK_SLOT_SAVE: + { + if (paintGetSlotInUse(paintState->index, paintState->selectedSlot)) + { + paintState->saveMenuBoolOption = false; + paintState->saveMenu = CONFIRM_OVERWRITE; + } + else + { + paintState->doSave = true; + } + break; + } + + case PICK_SLOT_LOAD: + { + if (paintState->unsaved) + { + paintState->saveMenuBoolOption = false; + paintState->saveMenu = CONFIRM_UNSAVED; + } + else + { + paintState->doLoad = true; + } + break; + } + + case CONFIRM_OVERWRITE: + { + if (paintState->saveMenuBoolOption) + { + paintState->doSave = true; + paintState->saveMenu = HIDDEN; + } + else + { + paintState->saveMenu = PICK_SLOT_SAVE; + } + break; + } + + case CONFIRM_UNSAVED: + { + if (paintState->saveMenuBoolOption) + { + paintState->doLoad = true; + paintState->saveMenu = HIDDEN; + } + else + { + paintState->saveMenu = PICK_SLOT_LOAD; + } + break; + } + + case EDIT_PALETTE: + { + paintState->saveMenu = COLOR_PICKER; + paintState->buttonMode = BTN_MODE_PALETTE; + paintState->paletteSelect = 0; + paintEditPaletteSetupColor(); + break; + } + + case CONFIRM_CLEAR: + { + if (paintState->saveMenuBoolOption) + { + paintStoreUndo(&paintState->canvas); + paintState->clearScreen = true; + paintState->saveMenu = HIDDEN; + paintState->buttonMode = BTN_MODE_DRAW; + } + else + { + paintState->saveMenu = CLEAR; + } + break; + } + + case CONFIRM_EXIT: + { + if (paintState->saveMenuBoolOption) + { + paintReturnToMainMenu(); + } + else + { + paintState->saveMenu = EXIT; + } + break; + } + + case CLEAR: + { + if (paintState->unsaved) + { + paintState->saveMenuBoolOption = false; + paintState->saveMenu = CONFIRM_CLEAR; + } + else + { + paintStoreUndo(&paintState->canvas); + paintState->clearScreen = true; + paintState->saveMenu = HIDDEN; + paintState->buttonMode = BTN_MODE_DRAW; + } + break; + } + case EXIT: + { + if (paintState->unsaved) + { + paintState->saveMenuBoolOption = false; + paintState->saveMenu = CONFIRM_EXIT; + } + else + { + paintReturnToMainMenu(); + } + break; + } + // These cases shouldn't actually happen + case HIDDEN: + case COLOR_PICKER: + { + paintState->buttonMode = BTN_MODE_DRAW; + break; + } + } + break; + } + + case PB_UP: + { + paintSaveModePrevItem(); + break; + } + + case PB_DOWN: + case PB_SELECT: + { + paintSaveModeNextItem(); + break; + } + + case PB_LEFT: + { + paintSaveModePrevOption(); + break; + } + + case PB_RIGHT: + { + paintSaveModeNextOption(); + break; + } + + case PB_B: + { + // Exit save menu + paintState->saveMenu = HIDDEN; + paintState->buttonMode = BTN_MODE_DRAW; + break; + } + + case PB_START: + // Handle this in button up + break; + } + } + else + { + //////// Save mode button release + if (evt->button == PB_START) + { + // Exit save menu + paintState->saveMenu = HIDDEN; + paintState->buttonMode = BTN_MODE_DRAW; + paintState->redrawToolbar = true; + } + } +} + +void paintSelectModeButtonCb(const buttonEvt_t* evt) +{ + if (!evt->down) + { + //////// Select-mode button release + switch (evt->button) + { + case PB_SELECT: + { + if (paintCanUndo()) + { + paintUndo(&paintState->canvas); + } + break; + } + + case PB_START: + { + if (paintCanRedo()) + { + paintRedo(&paintState->canvas); + } + break; + } + + case PB_UP: + { + // Select previous color + paintState->redrawToolbar = true; + paintState->paletteSelect = PREV_WRAP(paintState->paletteSelect, PAINT_MAX_COLORS); + paintUpdateLeds(); + break; + } + + case PB_DOWN: + { + // Select next color + paintState->redrawToolbar = true; + paintState->paletteSelect = NEXT_WRAP(paintState->paletteSelect, PAINT_MAX_COLORS); + paintUpdateLeds(); + break; + } + + case PB_LEFT: + { + // Select previous brush + paintPrevTool(); + paintState->redrawToolbar = true; + break; + } + + case PB_RIGHT: + { + // Select next brush + paintNextTool(); + paintState->redrawToolbar = true; + break; + } + + case PB_A: + { + // Increase brush size / next variant + paintIncBrushWidth(1); + paintState->redrawToolbar = true; + break; + } + + case PB_B: + { + // Decrease brush size / prev variant + paintDecBrushWidth(1); + paintState->redrawToolbar = true; + break; + } + } + } +} + +void paintDrawScreenPollTouch(void) +{ + int32_t centroid, intensity; + int32_t phi, r, y; + + if (getTouchJoystick(&phi, &r, &intensity)) + { + getTouchCartesian(phi, r, ¢roid, &y); + + paintState->toolWheel = wheelMenuTouch(paintState->toolWheel, paintState->toolWheelRenderer, phi, r); + return; + + /////////////////////////////// old code below, probs delete + + // Bar is touched + switch (paintState->buttonMode) + { + case BTN_MODE_DRAW: + case BTN_MODE_SELECT: + { + paintState->lastTouch = centroid; + + // Set up variables for swiping + if (!paintState->touchDown) + { + // Beginning of swipe + paintState->touchDown = true; + paintState->firstTouch = centroid; + + // Store the original brush width + paintState->startBrushWidth = getArtist()->brushWidth; + paintEnterSelectMode(); + + // Only call this here to prevent making a ton of unnecessary calls to paintTutorialOnEvent() + if (paintHelp != NULL) + { + // Don't worry about X or Y, we'll only decide those on release I guess + paintHelp->allButtons |= TOUCH_ANY; + // Replace the touch buttons, but not any of the real buttons + paintHelp->curButtons + = TOUCH_ANY + | (paintHelp->curButtons + & (PB_UP | PB_DOWN | PB_LEFT | PB_RIGHT | PB_A | PB_B | PB_START | PB_SELECT)); + paintHelp->lastButton = TOUCH_ANY; + paintHelp->lastButtonDown = true; + paintTutorialOnEvent(); + } + } + else + { + // We're mid-swipe + int32_t swipeMagnitude = ((paintState->firstTouch - centroid) * PAINT_MAX_BRUSH_SWIPE) / 1024; + int32_t newWidth = paintState->startBrushWidth - swipeMagnitude; + + if (newWidth < 0) + { + newWidth = 0; + } + else if (newWidth > UINT8_MAX) + { + newWidth = UINT8_MAX; + } + + paintSetBrushWidth((uint8_t)(newWidth)); + } + break; + } + + case BTN_MODE_PALETTE: + { + paintState->touchDown = true; + // Don't do anything for tutorial until release + uint8_t index = ((centroid * 5 + 512) / 1024); + // PAINT_LOGD("Centroid: %d, Intensity: %d, Index: %d", centroid, intensity, index); + paintEditPaletteSetChannelValue(index); + break; + } + + case BTN_MODE_SAVE: + break; + } + } + else + { + paintState->toolWheel = wheelMenuTouchRelease(paintState->toolWheel, paintState->toolWheelRenderer); + return; + + /////////////////////////////// old code below, probs delete + + // Bar is not touched + // Do not use centroid/intensity here + // And only do anything if paintState->touchDown is still true + if (paintState->touchDown) + { + paintState->touchDown = false; + + switch (paintState->buttonMode) + { + case BTN_MODE_DRAW: + case BTN_MODE_SELECT: + { + int32_t swipeMagnitude + = ((paintState->firstTouch - paintState->lastTouch) * PAINT_MAX_BRUSH_SWIPE) / 1024; + // PAINT_LOGD("End swipe: %d", swipeMagnitude); + if (swipeMagnitude == 0) + { + // Tap! But only if we started on X or Y + if (paintState->firstTouch < (1024 / 5)) + { + paintDecBrushWidth(1); + } + else if (paintState->firstTouch > (1024 * 4 / 5)) + { + paintIncBrushWidth(1); + } + } + + if (paintHelp != NULL) + { + paintHelp->curButtons + = paintHelp->curButtons + & (PB_UP | PB_DOWN | PB_LEFT | PB_RIGHT | PB_A | PB_B | PB_START | PB_SELECT); + paintHelp->lastButtonDown = false; + + if (swipeMagnitude == 0) + { + if (paintState->firstTouch < (1024 / 5)) + { + paintHelp->lastButton = TOUCH_Y; + } + else if (paintState->firstTouch > (1024 * 4 / 5)) + { + paintHelp->lastButton = TOUCH_X; + } + else + { + paintHelp->lastButton = TOUCH_ANY; + } + } + else if (swipeMagnitude > 0) + { + paintHelp->lastButton = SWIPE_RIGHT; + } + else // swipeMagnitude < 0 + { + paintHelp->lastButton = SWIPE_LEFT; + } + + paintTutorialOnEvent(); + } + + paintExitSelectMode(); + break; + } + + case BTN_MODE_PALETTE: + { + // Only do something in tutorial mode + if (paintHelp != NULL) + { + paintHelp->curButtons + = paintHelp->curButtons + & (PB_UP | PB_DOWN | PB_LEFT | PB_RIGHT | PB_A | PB_B | PB_START | PB_SELECT); + paintHelp->lastButtonDown = false; + paintHelp->lastButton = TOUCH_ANY; + paintTutorialOnEvent(); + } + + break; + } + + case BTN_MODE_SAVE: + break; + } + } + } +} + +void paintDrawScreenButtonCb(const buttonEvt_t* evt) +{ + if (paintHelp != NULL) + { + paintHelp->allButtons |= evt->state; + paintHelp->lastButton = evt->button; + paintHelp->lastButtonDown = evt->down; + // Keep the touch buttons in place but replace everything else with the button state + paintHelp->curButtons + = evt->state | (paintHelp->curButtons & (TOUCH_ANY | TOUCH_X | TOUCH_Y | SWIPE_LEFT | SWIPE_RIGHT)); + } + + if (wheelMenuActive(paintState->toolWheel, paintState->toolWheelRenderer)) + { + PAINT_LOGI("Menu is active, sending it a button press"); + wheelMenuButton(paintState->toolWheel, paintState->toolWheelRenderer, evt); + } + else + { + switch (paintState->buttonMode) + { + case BTN_MODE_DRAW: + { + paintDrawModeButtonCb(evt); + break; + } + + case BTN_MODE_SELECT: + { + paintSelectModeButtonCb(evt); + break; + } + + case BTN_MODE_SAVE: + { + paintSaveModeButtonCb(evt); + break; + } + + case BTN_MODE_PALETTE: + { + paintPaletteModeButtonCb(evt); + break; + } + } + } + + if (paintHelp != NULL) + { + paintTutorialOnEvent(); + } +} + +void paintDrawModeButtonCb(const buttonEvt_t* evt) +{ + if (evt->down) + { + // Draw mode buttons + switch (evt->button) + { + case PB_SELECT: + // SELECT no longer does anything + break; + + case PB_A: + { + // Draw + paintState->aHeld = true; + paintState->aPress = true; + break; + } + + case PB_B: + { + // Swap the foreground and background colors + paintSwapFgBgColors(); + + paintState->redrawToolbar = true; + paintState->recolorPickPoints = true; + break; + } + + case PB_UP: + case PB_DOWN: + case PB_LEFT: + case PB_RIGHT: + { + paintHandleDpad(evt->state & (PB_UP | PB_DOWN | PB_LEFT | PB_RIGHT)); + paintState->firstMove = true; + break; + } + + case PB_START: + // Don't do anything until start is released to avoid conflicting with EXIT + break; + } + } + else + { + //////// Draw mode button release + switch (evt->button) + { + case PB_START: + { + if (!paintState->saveInProgress) + { + // Enter the save menu + paintState->buttonMode = BTN_MODE_SAVE; + paintState->saveMenu = PICK_SLOT_SAVE; + paintState->redrawToolbar = true; + + // Don't let the cursor keep moving + paintState->moveX = 0; + paintState->moveY = 0; + paintState->btnHoldTime = 0; + paintState->aHeld = false; + } + break; + } + + case PB_A: + { + // Stop drawing + paintState->aHeld = false; + break; + } + + case PB_B: + // Do nothing; color swap is handled on button down + break; + + case PB_UP: + case PB_DOWN: + case PB_LEFT: + case PB_RIGHT: + { + paintHandleDpad(evt->state & (PB_UP | PB_DOWN | PB_LEFT | PB_RIGHT)); + break; + } + + case PB_SELECT: + // This is handled in BTN_MODE_SELECT already + break; + } + } +} + +void paintHandleDpad(uint16_t state) +{ + paintState->unhandledButtons |= state; + + if (!(state & PB_UP) != !(state & PB_DOWN)) + { + // Up or down, but not both, are pressed + paintState->moveY = (state & PB_DOWN) ? 1 : -1; + } + else + { + paintState->moveY = 0; + } + + if (!(state & PB_LEFT) != !(state & PB_RIGHT)) + { + // Left or right, but not both, are pressed + paintState->moveX = (state & PB_RIGHT) ? 1 : -1; + } + else + { + paintState->moveX = 0; + } + + if (!state) + { + // Reset the button hold time if all D-pad buttons are released + // This lets you make turns quickly instead of waiting for the repeat timeout in the middle + paintState->btnHoldTime = 0; + } +} + +void paintFreeUndos(void) +{ + paintState->undoHead = NULL; + for (node_t* undo = paintState->undoList.first; undo != NULL; undo = undo->next) + { + paintUndo_t* val = undo->val; + free(val); + } + clear(&paintState->undoList); +} + +void paintStoreUndo(paintCanvas_t* canvas) +{ + // If paintState->undoHead is set, we need to clear all the previous undos to delete the alternate timeline + uint8_t deleted = 0; + while (paintState->undoHead != NULL) + { + // Save the next pointer before the node gets freed + node_t* next = paintState->undoHead->next; + + paintUndo_t* delUndo = removeEntry(&paintState->undoList, paintState->undoHead); + + // Free the undo data pixels and then the struct itself + free(delUndo); + + paintState->undoHead = next; + deleted++; + } + if (deleted > 0) + { + PAINT_LOGD("Deleted %" PRIu8 " dangling undos after changing history", deleted); + } + // paintState->undoHead should now be NULL + + // Allocate a new paintUndo_t to store the canvas + paintUndo_t* undoData; + // Calculate the amount of space we wolud need to store the canvas pixels + size_t pxSize = paintGetStoredSize(canvas); + + // Allocate memory for the undo data struct and its pixel data in one go + void* undoMem = heap_caps_malloc(sizeof(paintUndo_t) + pxSize, MALLOC_CAP_SPIRAM); + if (undoMem != NULL) + { + // Alloc succeeded, use the data + undoData = undoMem; + undoData->px = (uint8_t*)undoMem + sizeof(paintUndo_t); + } + else + { + // Alloc failed, reuse the first undo data + undoData = shift(&paintState->undoList); + } + + if (!undoData) + { + PAINT_LOGD("Failed to allocate or reuse undo data! Canceling undo"); + // There's no undo data at all! We're completely out of space! + return; + } + + // Save the palette + memcpy(undoData->palette, canvas->palette, sizeof(paletteColor_t) * PAINT_MAX_COLORS); + + bool cursorVisible = getCursor()->show; + if (cursorVisible) + { + hideCursor(getCursor(), canvas); + } + // Save the pixel data + paintSerialize(undoData->px, canvas, 0, pxSize); + + if (cursorVisible) + { + showCursor(getCursor(), canvas); + } + + push(&paintState->undoList, undoData); +} + +// Delete the oldest undo entry, if one exists. Returns true if some space was made available, false otherwise. +bool paintMaybeSacrificeUndoForHeap(void) +{ + if (paintState->undoList.first != NULL) + { + // Don't leave a bad pointer in undoHead + // This has to be done *before* calling shift() + if (paintState->undoHead != NULL && paintState->undoHead == paintState->undoList.first) + { + paintState->undoHead = paintState->undoHead->next; + } + + paintUndo_t* delUndo = shift(&paintState->undoList); + + free(delUndo); + + return true; + } + + return false; +} + +bool paintCanUndo(void) +{ + // We can undo as long as one of these is true: + // - The undoHead is NULL and undoList.last is NOT NULL + // - The undoHead is NOT NULL and undoHead->prev is NOT NULL + return (paintState->undoHead == NULL && paintState->undoList.last != NULL) + || (paintState->undoHead != NULL && paintState->undoHead->prev != NULL); +} + +bool paintCanRedo(void) +{ + // We can redo as long as all of these are true: + // - The undoHead is NOT NULL + // - There is another undo after undoHead (that's what contains the state we want to return to) + return paintState->undoHead != NULL && paintState->undoHead->next != NULL; +} + +void paintApplyUndo(paintCanvas_t* canvas) +{ + if (paintState->undoHead == NULL) + { + // If we've undone everything, or there's nothing to undo, exit early + PAINT_LOGD("Not undoing because undoHead is NULL"); + return; + } + + hideCursor(getCursor(), canvas); + + paintUndo_t* undo = paintState->undoHead->val; + + memcpy(canvas->palette, undo->palette, sizeof(paletteColor_t) * PAINT_MAX_COLORS); + getArtist()->fgColor = canvas->palette[0]; + getArtist()->bgColor = canvas->palette[1]; + + size_t pxSize = paintGetStoredSize(canvas); + paintDeserialize(canvas, undo->px, 0, pxSize); + + PAINT_LOGD("Undid %" PRIu32 " bytes!", (uint32_t)pxSize); + + // feels weird to do this inside the undo functions... but it's probably ok? we've already undone anyway + showCursor(getCursor(), canvas); +} + +void paintUndo(paintCanvas_t* canvas) +{ + if (paintState->undoHead == NULL) + { + // We have not undone anything else yet -- use the last element in the undo list + node_t* head = paintState->undoList.last; + + // Also, since this is the first undo, save the current state so that we can return to it with redo + paintStoreUndo(canvas); + + paintState->undoHead = head; + } + else + { + // We have already undone something! Undo the previous action. + paintState->undoHead = paintState->undoHead->prev; + } + + paintApplyUndo(canvas); +} + +void paintRedo(paintCanvas_t* canvas) +{ + if (paintState->undoHead == NULL) + { + // We have not undone anything else -- so there's nothing to redo? + } + else + { + // We have already undone something, so there's something to redo + paintState->undoHead = paintState->undoHead->next; + } + + paintApplyUndo(canvas); +} + +bool paintSaveCanvas(paintCanvas_t* canvas) +{ + // Allocate a new paintUndo_t to store the canvas + paintUndo_t* undoData = paintState->storedCanvas; + + // Calculate the amount of space we wolud need to store the canvas pixels + size_t pxSize = paintGetStoredSize(canvas); + + if (!undoData) + { + // Allocate memory for the undo data struct and its pixel data in one go + void* undoMem = heap_caps_malloc(sizeof(paintUndo_t) + pxSize, MALLOC_CAP_SPIRAM); + if (undoMem != NULL) + { + // Alloc succeeded, use the data + undoData = undoMem; + undoData->px = (uint8_t*)undoMem + sizeof(paintUndo_t); + } + else + { + // Alloc failed, reuse the first undo data + undoData = shift(&paintState->undoList); + } + } + + if (!undoData) + { + PAINT_LOGD("Failed to allocate or reuse undo data! Can't save canvas"); + // There's no data at all! We're completely out of space! + return false; + } + + // Save the palette + memcpy(undoData->palette, canvas->palette, sizeof(paletteColor_t) * PAINT_MAX_COLORS); + + bool cursorVisible = getCursor()->show; + if (cursorVisible) + { + hideCursor(getCursor(), canvas); + } + // Save the pixel data + paintSerialize(undoData->px, canvas, 0, pxSize); + + if (cursorVisible) + { + showCursor(getCursor(), canvas); + } + + paintState->storedCanvas = undoData; + + return true; +} + +void paintRestoreCanvas(paintCanvas_t* canvas) +{ + if (NULL == paintState->storedCanvas) + { + return; + } + + // Don't restore palette + hideCursor(getCursor(), canvas); + + size_t pxSize = paintGetStoredSize(canvas); + paintDeserialize(canvas, paintState->storedCanvas->px, 0, pxSize); + memcpy(paintState->canvas.palette, paintState->storedCanvas->palette, sizeof(paletteColor_t) * PAINT_MAX_COLORS); + + // feels weird to do this inside the undo functions... but it's probably ok? we've already undone anyway + showCursor(getCursor(), canvas); +} + +void paintDoTool(uint16_t x, uint16_t y, paletteColor_t col) +{ + hideCursor(getCursor(), &paintState->canvas); + bool drawNow = false; + bool isLastPick = false; + + // Determine if this is the last pick for the tool + // This is so we don't draw a pick-marker that will be immediately removed + switch (getArtist()->brushDef->mode) + { + case PICK_POINT: + isLastPick = (pxStackSize(&getArtist()->pickPoints) + 1 == getArtist()->brushDef->maxPoints); + break; + + case PICK_POINT_LOOP: + isLastPick = pxStackSize(&getArtist()->pickPoints) + 1 == getArtist()->brushDef->maxPoints - 1; + break; + + case HOLD_DRAW: + break; + + default: + break; + } + + pushPxScaled(&getArtist()->pickPoints, getCursor()->x, getCursor()->y, paintState->canvas.x, paintState->canvas.y, + paintState->canvas.xScale, paintState->canvas.yScale); + + if (getArtist()->brushDef->mode == HOLD_DRAW) + { + drawNow = true; + } + else if (getArtist()->brushDef->mode == PICK_POINT || getArtist()->brushDef->mode == PICK_POINT_LOOP) + { + // Save the pixel underneath the selection, then draw a temporary pixel to mark it + // But don't bother if this is the last pick point, since it will never actually be seen + + if (getArtist()->brushDef->mode == PICK_POINT_LOOP) + { + pxVal_t firstPick, lastPick; + if (pxStackSize(&getArtist()->pickPoints) > 1 && getPx(&getArtist()->pickPoints, 0, &firstPick) + && peekPx(&getArtist()->pickPoints, &lastPick) && firstPick.x == lastPick.x + && firstPick.y == lastPick.y) + { + // If this isn't the first pick, and it's in the same position as the first pick, we're done! + drawNow = true; + } + else if (isLastPick) + { + // Special case: If we're on the next-to-last possible point, we have to add the start again as the last + // point + pushPx(&getArtist()->pickPoints, firstPick.x, firstPick.y); + + drawNow = true; + } + } + // only for non-loop brushes + else if (pxStackSize(&getArtist()->pickPoints) == getArtist()->brushDef->maxPoints) + { + drawNow = true; + } + } + + if (drawNow) + { + // Allocate an array of point_t for the canvas pick points + size_t pickCount = pxStackSize(&getArtist()->pickPoints); + point_t canvasPickPoints[sizeof(point_t) * pickCount]; + + // Convert the pick points into an array of canvas-coordinates + paintConvertPickPointsScaled(&getArtist()->pickPoints, &paintState->canvas, canvasPickPoints); + + while (popPxScaled(&getArtist()->pickPoints, paintState->canvas.xScale, paintState->canvas.yScale)) + ; + + // Save the current state before we draw, but only do it on the first press if we're using a HOLD_DRAW pen + if (getArtist()->brushDef->mode != HOLD_DRAW || paintState->aPress) + { + paintStoreUndo(&paintState->canvas); + } + + paintState->unsaved = true; + getArtist()->brushDef->fnDraw(&paintState->canvas, canvasPickPoints, pickCount, getArtist()->brushWidth, col); + + if (paintHelp != NULL) + { + paintHelp->drawComplete = true; + } + } + else + { + // A bit counterintuitively, this will restart the blink timer on the next frame + paintState->blinkTimer = BLINK_TIME_OFF; + paintState->blinkOn = false; + } + + showCursor(getCursor(), &paintState->canvas); + paintRenderToolbar(getArtist(), &paintState->canvas, paintState, firstBrush, lastBrush); +} + +void paintSetupTool(void) +{ + // Reset the brush params + if (getArtist()->brushWidth < getArtist()->brushDef->minSize) + { + getArtist()->brushWidth = getArtist()->brushDef->minSize; + paintState->startBrushWidth = getArtist()->brushWidth; + } + else if (getArtist()->brushWidth > getArtist()->brushDef->maxSize) + { + getArtist()->brushWidth = getArtist()->brushDef->maxSize; + paintState->startBrushWidth = getArtist()->brushWidth; + } + + hideCursor(getCursor(), &paintState->canvas); + paintHidePickPoints(); + switch (getArtist()->brushDef->mode) + { + case HOLD_DRAW: + { + // Regenerate the cursor if it's not been set yet or if the brush's size is different from the cursor's size + if (paintState->cursorWsg.px == NULL + || paintState->cursorWsg.w != (getArtist()->brushWidth * paintState->canvas.xScale + 2) + || paintState->cursorWsg.h != (getArtist()->brushWidth * paintState->canvas.yScale + 2)) + { + paintFreeCursorSprite(&paintState->cursorWsg); + paintGenerateCursorSprite(&paintState->cursorWsg, &paintState->canvas, getArtist()->brushWidth); + } + + setCursorSprite(getCursor(), &paintState->canvas, &paintState->cursorWsg); + // Center the cursor, accounting for even and odd cursor sizes + setCursorOffset(getCursor(), -(paintState->cursorWsg.w / 2) + getArtist()->brushWidth % 2, + -(paintState->cursorWsg.h / 2) + getArtist()->brushWidth % 2); + break; + } + + case PICK_POINT: + case PICK_POINT_LOOP: + { + setCursorSprite(getCursor(), &paintState->canvas, &paintState->picksWsg); + // Place the top-right pixel of the pointer 1px inside the target pixel + setCursorOffset(getCursor(), -paintState->picksWsg.w + 1, paintState->canvas.yScale - 1); + break; + } + } + + // Undraw and hide any stored temporary pixels + while (popPxScaled(&getArtist()->pickPoints, paintState->canvas.xScale, paintState->canvas.yScale)) + ; + showCursor(getCursor(), &paintState->canvas); +} + +void paintPrevTool(void) +{ + if (getArtist()->brushDef == firstBrush) + { + getArtist()->brushDef = lastBrush; + } + else + { + getArtist()->brushDef--; + } + + paintSetupTool(); +} + +void paintNextTool(void) +{ + if (getArtist()->brushDef == lastBrush) + { + getArtist()->brushDef = firstBrush; + } + else + { + getArtist()->brushDef++; + } + + paintSetupTool(); +} + +void paintSetBrushWidth(uint8_t width) +{ + if (width < getArtist()->brushDef->minSize) + { + getArtist()->brushWidth = getArtist()->brushDef->minSize; + } + else if (width > getArtist()->brushDef->maxSize) + { + getArtist()->brushWidth = getArtist()->brushDef->maxSize; + } + else + { + getArtist()->brushWidth = width; + } + + paintSetupTool(); + paintState->redrawToolbar = true; +} + +void paintDecBrushWidth(uint8_t dec) +{ + if (getArtist()->brushWidth <= dec || getArtist()->brushWidth <= getArtist()->brushDef->minSize) + { + getArtist()->brushWidth = getArtist()->brushDef->minSize; + } + else + { + getArtist()->brushWidth -= dec; + } + + paintSetupTool(); + paintState->redrawToolbar = true; +} + +void paintIncBrushWidth(uint8_t inc) +{ + getArtist()->brushWidth += inc; + + if (getArtist()->brushWidth > getArtist()->brushDef->maxSize) + { + getArtist()->brushWidth = getArtist()->brushDef->maxSize; + } + + paintSetupTool(); + paintState->redrawToolbar = true; +} + +void paintSwapFgBgColors(void) +{ + uint8_t fgIndex = 0, bgIndex = 0; + swap(&getArtist()->fgColor, &getArtist()->bgColor); + + for (uint8_t i = 0; i < PAINT_MAX_COLORS; i++) + { + if (paintState->canvas.palette[i] == getArtist()->fgColor) + { + fgIndex = i; + } + else if (paintState->canvas.palette[i] == getArtist()->bgColor) + { + bgIndex = i; + } + } + + for (uint8_t i = fgIndex; i > 0; i--) + { + if (i == bgIndex) + { + continue; + } + paintState->canvas.palette[i] = paintState->canvas.palette[i - 1 + ((i < bgIndex) ? 1 : 0)]; + } + + paintState->canvas.palette[0] = getArtist()->fgColor; + + paintUpdateLeds(); + paintDrawPickPoints(); +} + +void paintEnterSelectMode(void) +{ + if (paintState->buttonMode == BTN_MODE_SELECT && paintState->showToolWheel) + { + return; + } + + if (!paintState->showToolWheel) + { + paintSaveCanvas(&paintState->canvas); + paintState->showToolWheel = true; + } + + paintState->buttonMode = BTN_MODE_SELECT; + paintState->redrawToolbar = true; + paintState->aHeld = false; + paintState->moveX = 0; + paintState->moveY = 0; + paintState->btnHoldTime = 0; + paintSetupColorWheel(); +} + +void paintExitSelectMode(void) +{ + if (paintState->buttonMode == BTN_MODE_DRAW && !paintState->showToolWheel) + { + return; + } + + // Exit select mode + paintState->buttonMode = BTN_MODE_DRAW; + + if (paintState->showToolWheel) + { + paintRestoreCanvas(&paintState->canvas); + paintState->showToolWheel = false; + } + + // Set the current selection as the FG color and rearrange the rest + paintUpdateRecents(paintState->paletteSelect); + paintState->paletteSelect = 0; + + paintState->redrawToolbar = true; +} + +void paintUpdateRecents(uint8_t selectedIndex) +{ + getArtist()->fgColor = paintState->canvas.palette[selectedIndex]; + + for (uint8_t i = selectedIndex; i > 0; i--) + { + paintState->canvas.palette[i] = paintState->canvas.palette[i - 1]; + } + paintState->canvas.palette[0] = getArtist()->fgColor; + + paintUpdateLeds(); + + // If there are any pick points, update their color to reduce confusion + paintDrawPickPoints(); +} + +void paintUpdateLeds(void) +{ + uint32_t rgb = 0; + + // Only set the LED color if LEDs are enabled + if (paintState->index & PAINT_ENABLE_LEDS) + { + if (paintState->buttonMode == BTN_MODE_PALETTE) + { + // Show the edited color if we're editing the palette + rgb = paletteToRGB(paintState->newColor); + } + else if (paintState->buttonMode == BTN_MODE_SELECT) + { + // Show the selected color if we're picking colors + rgb = paletteToRGB(paintState->canvas.palette[paintState->paletteSelect]); + } + else + { + // Otherwise, use the current draw color + rgb = paletteToRGB(getArtist()->fgColor); + } + } + + for (uint8_t i = 0; i < CONFIG_NUM_LEDS; i++) + { + paintState->leds[i].b = (rgb >> 0) & 0xFF; + paintState->leds[i].g = (rgb >> 8) & 0xFF; + paintState->leds[i].r = (rgb >> 16) & 0xFF; + } + + setLeds(paintState->leds, CONFIG_NUM_LEDS); +} + +void paintDrawPickPoints(void) +{ + pxVal_t point; + for (size_t i = 0; i < pxStackSize(&getArtist()->pickPoints); i++) + { + if (getPx(&getArtist()->pickPoints, i, &point)) + { + bool invert = (i == 0 && getArtist()->brushDef->mode == PICK_POINT_LOOP) + && pxStackSize(&getArtist()->pickPoints) > 1; + drawRectFilled(point.x, point.y, point.x + paintState->canvas.xScale + 1, + point.y + paintState->canvas.yScale + 1, + invert ? getContrastingColor(point.col) : getArtist()->fgColor); + } + } +} + +void paintHidePickPoints(void) +{ + pxVal_t point; + for (size_t i = 0; i < pxStackSize(&getArtist()->pickPoints); i++) + { + if (getPx(&getArtist()->pickPoints, i, &point)) + { + drawRectFilled(point.x, point.y, point.x + paintState->canvas.xScale + 1, + point.y + paintState->canvas.yScale + 1, point.col); + } + } +} + +paintArtist_t* getArtist(void) +{ + // TODO: Take player order into account + return paintState->artist; +} + +paintCursor_t* getCursor(void) +{ + // TODO Take player order into account + return &paintState->artist->cursor; +} + +static void paintToolWheelCb(const char* label, bool selected, uint32_t settingVal) +{ + if (NULL == label) + { + return; + } + + if (selected) + { + PAINT_LOGI("Selected tool wheel item %s", label); + if (toolWheelUndoStr == label) + { + paintExitSelectMode(); + if (paintCanUndo()) + { + paintUndo(&paintState->canvas); + } + } + else if (toolWheelRedoStr == label) + { + paintExitSelectMode(); + if (paintCanRedo()) + { + paintRedo(&paintState->canvas); + } + } + else if (toolWheelSaveStr == label) + { + if (paintGetSlotInUse(paintState->index, paintState->selectedSlot)) + { + paintState->saveMenuBoolOption = false; + paintState->saveMenu = CONFIRM_OVERWRITE; + } + else + { + paintState->doSave = true; + } + } + // Check if the label is one of the color name strings + else if (NULL != label && paintState->colorNames[0] <= label + && label <= paintState->colorNames[PAINT_MAX_COLORS - 1]) + { + uint8_t colorIndex = (label - *paintState->colorNames) / sizeof(*paintState->colorNames); + paintState->paletteSelect = colorIndex; + } + } + else + { + if (toolWheelBrushStr == label) + { + paintEnterSelectMode(); + + getArtist()->brushDef = (firstBrush + settingVal); + paintSetupTool(); + paintState->redrawToolbar = true; + } + else if (toolWheelColorStr == label) + { + paintEnterSelectMode(); + // Select previous color + paintState->redrawToolbar = true; + paintState->paletteSelect = settingVal; + paintUpdateLeds(); + } + else if (toolWheelSizeStr == label) + { + paintEnterSelectMode(); + paintSetBrushWidth(settingVal); + } + else if (paintState->colorNames[0] <= label && label <= paintState->colorNames[PAINT_MAX_COLORS - 1]) + { + paintEnterSelectMode(); + // color, do something? + uint8_t colorIndex = (label - *paintState->colorNames) / sizeof(*paintState->colorNames); + paintState->paletteSelect = colorIndex; + } + else + { + // Check for all the brush names + for (const brush_t* brush = firstBrush; brush <= lastBrush; brush++) + { + if (brush->name == label) + { + paintEnterSelectMode(); + getArtist()->brushDef = brush; + paintSetupTool(); + paintState->redrawToolbar = true; + return; + } + } + + // Something else: + } + PAINT_LOGI("Moved to tool wheel item %s", label); + } +} \ No newline at end of file diff --git a/main/modes/mfpaint/paint_draw.h b/main/modes/mfpaint/paint_draw.h new file mode 100644 index 000000000..9b80633a0 --- /dev/null +++ b/main/modes/mfpaint/paint_draw.h @@ -0,0 +1,84 @@ +#ifndef _PAINT_DRAW_H_ +#define _PAINT_DRAW_H_ + +#include "palette.h" +#include "swadge2024.h" + +#include "paint_common.h" +#include "paint_brush.h" +#include "paint_help.h" + +extern paintDraw_t* paintState; + +// Mode callback delegates +void paintDrawScreenMainLoop(int64_t elapsedUs); +void paintDrawScreenButtonCb(const buttonEvt_t* evt); +void paintDrawScreenPollTouch(void); +void paintPaletteModeButtonCb(const buttonEvt_t* evt); +void paintSaveModeButtonCb(const buttonEvt_t* evt); +void paintSelectModeButtonCb(const buttonEvt_t* evt); +void paintDrawModeButtonCb(const buttonEvt_t* evt); + +// Palette mode helpers +void paintEditPaletteUpdate(void); +void paintEditPaletteSetChannelValue(uint8_t val); +// void paintEditPaletteDecChannel(void); +void paintEditPaletteIncChannel(void); +void paintEditPaletteNextChannel(void); + +void paintEditPalettePrevChannel(void); +void paintEditPaletteSetupColor(void); +void paintEditPalettePrevColor(void); +void paintEditPaletteNextColor(void); +void paintEditPaletteConfirm(void); + +// Save menu helpers +void paintSaveModePrevItem(void); +void paintSaveModeNextItem(void); +void paintSaveModePrevOption(void); +void paintSaveModeNextOption(void); + +// Setup/cleanup functions +void paintDrawScreenSetup(void); +void paintDrawScreenCleanup(void); +void paintTutorialSetup(void); +void paintTutorialPostSetup(void); +void paintTutorialCleanup(void); +void paintTutorialOnEvent(void); +bool paintTutorialCheckTrigger(const paintHelpTrigger_t* trigger); + +void paintPositionDrawCanvas(void); + +void paintHandleDpad(uint16_t state); +void paintFreeUndos(void); +void paintStoreUndo(paintCanvas_t* canvas); +bool paintMaybeSacrificeUndoForHeap(void); +bool paintCanUndo(void); +bool paintCanRedo(void); +void paintApplyUndo(paintCanvas_t* canvas); +void paintUndo(paintCanvas_t* canvas); +void paintRedo(paintCanvas_t* canvas); +bool paintSaveCanvas(paintCanvas_t* canvas); +void paintRestoreCanvas(paintCanvas_t* canvas); +void paintDoTool(uint16_t x, uint16_t y, paletteColor_t col); +void paintSwapFgBgColors(void); +void paintEnterSelectMode(void); +void paintExitSelectMode(void); +void paintUpdateRecents(uint8_t selectedIndex); +void paintUpdateLeds(void); +void paintDrawPickPoints(void); +void paintHidePickPoints(void); + +// Brush Helper Functions +void paintSetupTool(void); +void paintPrevTool(void); +void paintNextTool(void); +void paintSetBrushWidth(uint8_t width); +void paintDecBrushWidth(uint8_t dec); +void paintIncBrushWidth(uint8_t inc); + +// Artist helpers +paintArtist_t* getArtist(void); +paintCursor_t* getCursor(void); + +#endif diff --git a/main/modes/mfpaint/paint_gallery.c b/main/modes/mfpaint/paint_gallery.c new file mode 100644 index 000000000..261600730 --- /dev/null +++ b/main/modes/mfpaint/paint_gallery.c @@ -0,0 +1,402 @@ +#include "paint_gallery.h" + +#include "settingsManager.h" + +#include "mode_paint.h" +#include "paint_common.h" +#include "paint_nvs.h" +#include "paint_util.h" +#include "touchUtils.h" + +#include + +static const char transitionTime[] = "Slideshow: %g sec"; +static const char transitionOff[] = "Slideshow: Off"; +static const char transitionTimeNvsKey[] = "paint_gal_time"; +// static const char danceIndexKey[] = "paint_dance_idx"; + +// Possible transition times, in ms +static const uint16_t transitionTimeMap[] = { + 0, // off + 500, 1000, 2000, 5000, 10000, 15000, 30000, 45000, 60000, +}; + +// Denotes 5s +#define DEFAULT_TRANSITION_INDEX 4 + +#define US_PER_SEC 1000000 +#define US_PER_MS 1000 + +// 3s for info text to stay up +#define GALLERY_INFO_TIME 3000000 +#define GALLERY_INFO_Y_MARGIN 13 +#define GALLERY_INFO_X_MARGIN 13 +#define GALLERY_ARROW_MARGIN 6 + +paintGallery_t* paintGallery; + +static int16_t arrowCharToRot(char dir); + +void paintGallerySetup(bool screensaver) +{ + paintGallery = calloc(sizeof(paintGallery_t), 1); + paintGallery->galleryTime = 0; + paintGallery->galleryLoadNew = true; + paintGallery->screensaverMode = screensaver; + // Show the UI at the start if we're a screensaver + paintGallery->showUi = !screensaver; + loadFont("ibm_vga8.font", &paintGallery->infoFont, false); + loadWsg("arrow12.wsg", &paintGallery->arrow, false); + + // Recolor the arrow to black + colorReplaceWsg(&paintGallery->arrow, c555, c000); + + paintLoadIndex(&paintGallery->index); + + if (paintGetAnySlotInUse(paintGallery->index) && paintGetRecentSlot(paintGallery->index) != PAINT_SAVE_SLOTS) + { + paintGallery->gallerySlot = paintGetRecentSlot(paintGallery->index); + PAINT_LOGD("Using the most recent gallery slot: %d", paintGallery->gallerySlot); + } + else + { + paintGallery->gallerySlot = paintGetNextSlotInUse(paintGallery->index, PAINT_SAVE_SLOTS - 1); + PAINT_LOGD("Using the first slot: %d", paintGallery->gallerySlot); + } + + if (!readNvs32(transitionTimeNvsKey, &paintGallery->gallerySpeedIndex)) + { + // Default of 5s if not yet set + paintGallery->gallerySpeedIndex = DEFAULT_TRANSITION_INDEX; + } + + if (paintGallery->gallerySpeedIndex < 0 + || paintGallery->gallerySpeedIndex >= sizeof(transitionTimeMap) / sizeof(*transitionTimeMap)) + { + paintGallery->gallerySpeedIndex = DEFAULT_TRANSITION_INDEX; + } + + paintGallery->gallerySpeed = US_PER_MS * transitionTimeMap[paintGallery->gallerySpeedIndex]; + + /* TODO: Add this back when LED dances are solved + paintGallery->portableDances = initPortableDance(danceIndexKey); + portableDanceDisableDance(paintGallery->portableDances, "Flashlight"); + + if (!(paintGallery->index & PAINT_ENABLE_LEDS)) + { + portableDanceSetByName(paintGallery->portableDances, "None"); + } + */ + + // clear LEDs, which might still be set by menu + led_t leds[CONFIG_NUM_LEDS]; + memset(leds, 0, sizeof(led_t) * CONFIG_NUM_LEDS); + setLeds(leds, CONFIG_NUM_LEDS); +} + +void paintGalleryCleanup(void) +{ + freeFont(&paintGallery->infoFont); + freeWsg(&paintGallery->arrow); + // freePortableDance(paintGallery->portableDances); + free(paintGallery); +} + +void paintGalleryMainLoop(int64_t elapsedUs) +{ + paintGalleryModePollTouch(); + // portableDanceMainLoop(paintGallery->portableDances, elapsedUs); + + if (paintGallery->infoTimeRemaining > 0) + { + paintGallery->infoTimeRemaining -= elapsedUs; + + if (paintGallery->infoTimeRemaining <= 0) + { + paintGallery->infoTimeRemaining = 0; + paintGallery->galleryLoadNew = true; + paintGallery->showUi = false; + } + } + else + { + if (paintGallery->gallerySpeed != 0 && paintGallery->galleryTime >= paintGallery->gallerySpeed) + { + uint8_t prevSlot = paintGallery->gallerySlot; + paintGallery->gallerySlot = paintGetNextSlotInUse(paintGallery->index, paintGallery->gallerySlot); + // Only load the next image if it's actually a different image + paintGallery->galleryLoadNew = (paintGallery->gallerySlot != prevSlot); + paintGallery->galleryTime %= paintGallery->gallerySpeed; + + // reset info time if we're going to transition and clear the screen + paintGallery->infoTimeRemaining = 0; + } + + paintGallery->galleryTime += elapsedUs; + } + + if (paintGallery->galleryLoadNew) + { + paintGallery->galleryLoadNew = false; + if (!paintGalleryDoLoad()) + { + return; + } + } + + if (paintGallery->showUi) + { + paintGallery->showUi = false; + paintGalleryDrawUi(); + } +} + +static int16_t arrowCharToRot(char dir) +{ + switch (dir) + { + case 'L': + case 'l': + return 270; + + case 'R': + case 'r': + return 90; + + case 'D': + case 'd': + return 180; + + case 'U': + case 'u': + default: + return 0; + } +} + +void paintGalleryDrawUi(void) +{ + char text[32]; + + snprintf(text, sizeof(text), "A: Next Slide B: Exit"); + paintGalleryAddInfoText(text, 0, false, 0, 0); + + snprintf(text, sizeof(text), "Select: Scale: %dx", paintGallery->galleryScale); + paintGalleryAddInfoText(text, 1, false, 0, 0); + + // Draw the controls + snprintf(text, sizeof(text), "Y~X: LED Brightness: %d", getLedBrightnessSetting()); + paintGalleryAddInfoText(text, 2, false, 0, 0); + + // Draw speed 2 rows from the bottom + if (paintGallery->gallerySpeed == 0) + { + paintGalleryAddInfoText(transitionOff, -3, true, 'U', 0); + } + else + { + snprintf(text, sizeof(text), transitionTime, (1.0 * paintGallery->gallerySpeed / US_PER_SEC)); + paintGalleryAddInfoText( + text, -3, true, + (paintGallery->gallerySpeedIndex + 1 < sizeof(transitionTimeMap) / sizeof(*transitionTimeMap)) ? 'U' : 0, + 'D'); + } + + // Draw the LED dance at the bottom + // snprintf(text, sizeof(text), "LEDs: %s", portableDanceGetName(paintGallery->portableDances)); + // paintGalleryAddInfoText(text, -1, true, 'L', 'R'); +} + +void paintGalleryAddInfoText(const char* text, int8_t row, bool center, char leftArrow, char rightArrow) +{ + uint16_t width = textWidth(&paintGallery->infoFont, text); + uint16_t padding = 3; + int16_t yOffset; + int16_t xOffset = center ? ((TFT_WIDTH - width) / 2) : GALLERY_INFO_X_MARGIN; + + if (row < 0) + { + yOffset = TFT_WIDTH + ((row) * (paintGallery->infoFont.height + padding * 2)) - GALLERY_INFO_Y_MARGIN; + } + else + { + yOffset = GALLERY_INFO_Y_MARGIN + row * (paintGallery->infoFont.height + padding * 2); + } + + fillDisplayArea(0, yOffset, TFT_WIDTH, yOffset + padding * 2 + paintGallery->infoFont.height, c555); + + if (leftArrow != 0) + { + // assumes arrows are always square, flip between W and H if that's ever the case + drawWsg(&paintGallery->arrow, xOffset - GALLERY_ARROW_MARGIN - paintGallery->arrow.w, + yOffset + padding + (paintGallery->infoFont.height - paintGallery->arrow.h) / 2, false, false, + arrowCharToRot(leftArrow)); + } + + if (rightArrow != 0) + { + // assumes arrows are always square, flip between W and H if that's ever the case + drawWsg(&paintGallery->arrow, xOffset + width + GALLERY_ARROW_MARGIN, + yOffset + padding + (paintGallery->infoFont.height - paintGallery->arrow.h) / 2, false, false, + arrowCharToRot(rightArrow)); + } + + drawText(&paintGallery->infoFont, c000, text, xOffset, yOffset + padding); + + // start the timer to clear the screen + paintGallery->infoTimeRemaining = GALLERY_INFO_TIME; +} + +void paintGalleryDecreaseSpeed(void) +{ + if (paintGallery->gallerySpeedIndex + 1 < sizeof(transitionTimeMap) / sizeof(*transitionTimeMap)) + { + paintGallery->gallerySpeedIndex++; + paintGallery->gallerySpeed = US_PER_MS * transitionTimeMap[paintGallery->gallerySpeedIndex]; + writeNvs32(transitionTimeNvsKey, paintGallery->gallerySpeedIndex); + + paintGallery->galleryTime = 0; + } +} + +void paintGalleryIncreaseSpeed(void) +{ + if (paintGallery->gallerySpeedIndex > 0) + { + paintGallery->gallerySpeedIndex--; + paintGallery->gallerySpeed = US_PER_MS * transitionTimeMap[paintGallery->gallerySpeedIndex]; + writeNvs32(transitionTimeNvsKey, paintGallery->gallerySpeedIndex); + + paintGallery->galleryTime = 0; + } +} + +void paintGalleryModeButtonCb(buttonEvt_t* evt) +{ + uint8_t prevSlot = paintGallery->gallerySlot; + + if (evt->down) + { + if (paintGallery->screensaverMode) + { + // Return to main menu immediately + paintReturnToMainMenu(); + return; + } + + switch (evt->button) + { + case PB_UP: + { + paintGallery->showUi = true; + paintGalleryDecreaseSpeed(); + break; + } + + case PB_DOWN: + { + paintGallery->showUi = true; + paintGalleryIncreaseSpeed(); + break; + } + + case PB_LEFT: + { + // portableDancePrev(paintGallery->portableDances); + paintGallery->showUi = true; + break; + } + + case PB_RIGHT: + { + // portableDanceNext(paintGallery->portableDances); + paintGallery->showUi = true; + break; + } + + case PB_START: + case PB_B: + { + // Exit + paintReturnToMainMenu(); + return; + } + + case PB_SELECT: + { + // Increase size + paintGallery->galleryScale++; + paintGallery->galleryLoadNew = true; + paintGallery->showUi = true; + break; + } + + case PB_A: + { + paintGallery->gallerySlot = paintGetNextSlotInUse(paintGallery->index, paintGallery->gallerySlot); + paintGallery->galleryLoadNew = (prevSlot != paintGallery->gallerySlot); + if (paintGallery->galleryLoadNew) + { + paintSetRecentSlot(&paintGallery->index, paintGallery->gallerySlot); + } + paintGallery->galleryTime = 0; + break; + } + } + } +} + +void paintGalleryModePollTouch(void) +{ + int32_t centroid, intensity; + + int32_t angle, radius; + if (getTouchJoystick(&angle, &radius, &intensity)) + { + // TODO: Use the touchpad properly + getTouchCartesian(angle, radius, ¢roid, NULL); + // Bar is touched, convert the centroid into 8 segments (0-7) + // But also reverse it so up is bright and down is less bright + uint8_t curTouchSegment = ((centroid * 7 + 512) / 1024); + + if (curTouchSegment != getLedBrightnessSetting()) + { + setLedBrightnessSetting(curTouchSegment); + paintGallery->showUi = true; + } + } +} + +bool paintGalleryDoLoad(void) +{ + if (paintLoadDimensions(&paintGallery->canvas, paintGallery->gallerySlot)) + { + uint8_t maxScale = paintGetMaxScale(paintGallery->canvas.w, paintGallery->canvas.h, 0, 0); + + if (paintGallery->galleryScale > maxScale) + { + paintGallery->galleryScale = 1; + } + else if (paintGallery->galleryScale == 0) + { + paintGallery->galleryScale = maxScale; + } + + paintGallery->canvas.xScale = paintGallery->galleryScale; + paintGallery->canvas.yScale = paintGallery->galleryScale; + + paintGallery->canvas.x = (TFT_WIDTH - paintGallery->canvas.w * paintGallery->canvas.xScale) / 2; + paintGallery->canvas.y = (TFT_HEIGHT - paintGallery->canvas.h * paintGallery->canvas.yScale) / 2; + + clearPxTft(); + + return paintLoad(&paintGallery->index, &paintGallery->canvas, paintGallery->gallerySlot); + } + else + { + PAINT_LOGE("Slot %d has 0 dimension! Stopping load and clearing slot", paintGallery->gallerySlot); + paintClearSlot(&paintGallery->index, paintGallery->gallerySlot); + paintReturnToMainMenu(); + return false; + } +} diff --git a/main/modes/mfpaint/paint_gallery.h b/main/modes/mfpaint/paint_gallery.h new file mode 100644 index 000000000..c9ed539de --- /dev/null +++ b/main/modes/mfpaint/paint_gallery.h @@ -0,0 +1,26 @@ +#ifndef _PAINT_GALLERY_H_ +#define _PAINT_GALLERY_H_ + +#include + +#include "swadge2024.h" + +#include "paint_common.h" + +extern paintGallery_t* paintGallery; + +void paintGallerySetup(bool screensaver); +void paintGalleryCleanup(void); +void paintGalleryMainLoop(int64_t elapsedUs); +void paintGalleryModeButtonCb(buttonEvt_t* evt); + +void paintGalleryModePollTouch(void); + +void paintGalleryDrawUi(void); +void paintGalleryAddInfoText(const char* text, int8_t row, bool center, char leftArrow, char rightArrow); +void paintGalleryDecreaseSpeed(void); +void paintGalleryIncreaseSpeed(void); + +bool paintGalleryDoLoad(void); + +#endif diff --git a/main/modes/mfpaint/paint_help.c b/main/modes/mfpaint/paint_help.c new file mode 100644 index 000000000..faff7de72 --- /dev/null +++ b/main/modes/mfpaint/paint_help.c @@ -0,0 +1,230 @@ +#include "paint_help.h" + +#include "hdw-btn.h" + +/* + * Interactive Help Definitions + * + * Each step here defines a tutorial step with a help message that will + * be displayed below the canvas, and a trigger that will cause the tutorial + * step to be considered "completed" and move on to the next step. + */ + +static const char STR_HOLD_A_TO_DRAW[] + = "Cool! You can also hold A to draw while moving with the D-Pad. Let's try it! Hold A and press D-Pad DOWN"; +static const char STR_RELEASE_TOUCH_PAD_TO_CONFIRM[] = "And release the TOUCH PAD to confirm!"; + +static const char STR_BRUSH_RECTANGLE[] = "Rectangle"; +static const char STR_BRUSH_POLYGON[] = "Polygon"; + +const paintHelpStep_t helpSteps[] = +{ + { + .trigger = { .type = PRESS_ALL, .data = (PB_UP | PB_DOWN | PB_LEFT | PB_RIGHT) }, + .prompt = "Welcome to MFPaint!\nLet's get started: First, use the D-Pad to move the cursor around!" + }, + { + .trigger = { .type = RELEASE, .data = PB_A, }, + .prompt = "Excellent!\nNow, press A to draw something!" + }, + { + .trigger = { .type = PRESS, .data = (PB_A | PB_DOWN), }, + .prompt = STR_HOLD_A_TO_DRAW + }, + { + .trigger = { .type = RELEASE, .data = PB_DOWN, }, + .prompt = STR_HOLD_A_TO_DRAW + }, + { + .trigger = { .type = PRESS, .data = TOUCH_ANY, }, + .prompt = "Magnificent! But what if you make a mistake? Worry not, you can UNDO! Press and hold the TOUCH PAD between Y and X..." + }, + { + .trigger = { .type = PRESS, .data = TOUCH_ANY | PB_SELECT, }, + .backtrack = { .type = RELEASE, .data = TOUCH_ANY | SWIPE_LEFT | SWIPE_RIGHT | TOUCH_X | TOUCH_Y | PB_SELECT }, + .backtrackSteps = 1, + .prompt = "And then press SELECT to UNDO the last action" + }, + { + .trigger = { .type = PRESS, .data = TOUCH_ANY, }, + .prompt = "Phew! You can also REDO by holding the TOUCH PAD again..." + }, + { + .trigger = { .type = PRESS, .data = TOUCH_ANY | PB_START, }, + .backtrack = { .type = RELEASE, .data = TOUCH_ANY | SWIPE_LEFT | SWIPE_RIGHT | TOUCH_X | TOUCH_Y | PB_START }, + .backtrackSteps = 1, + .prompt = "And then pressing START to REDO what was undone" + }, + { + .trigger = { .type = PRESS, .data = TOUCH_ANY, }, + .prompt = "Now, let's change the color. Press and hold the TOUCH PAD again..." + }, + { + .trigger = { .type = PRESS, .data = TOUCH_ANY | PB_DOWN, }, + .backtrack = { .type = RELEASE, .data = TOUCH_ANY | SWIPE_LEFT | SWIPE_RIGHT | TOUCH_X | TOUCH_Y }, + .backtrackSteps = 1, + .prompt = "Then, press D-Pad DOWN to change the color selection..." + }, + { + .trigger = { .type = RELEASE, .data = TOUCH_ANY | TOUCH_X | TOUCH_Y | SWIPE_LEFT | SWIPE_RIGHT }, + .prompt = STR_RELEASE_TOUCH_PAD_TO_CONFIRM }, + { + .trigger = { .type = RELEASE, .data = PB_B, }, + .prompt = "Great choice! You can also quickly swap the foreground and background colors with the B BUTTON" + }, + { + .trigger = { .type = RELEASE, .data = TOUCH_X, }, + .prompt = "Now, let's change the brush size. Just tap X on the TOUCH PAD to increase the brush size by 1" + }, + { + .trigger = { .type = RELEASE, .data = PB_A, }, + .prompt = "Press A to draw again with the larger brush!" + }, + { + .trigger = { .type = RELEASE, .data = TOUCH_Y, }, + .prompt = "Wow! Now, to decrease the brush size, just tap Y on the TOUCH PAD!" + }, + { + .trigger = { .type = RELEASE, .data = SWIPE_LEFT, }, + .prompt = "You can also increase the brush size smoothly by swiping RIGHT (from Y to X) on the TOUCH PAD" + }, + { + .trigger = { .type = RELEASE, .data = SWIPE_RIGHT, }, + .prompt = "And you can decrease it smoothly by swiping LEFT (from X to Y) on the TOUCH PAD" + }, + { + .trigger = { .type = PRESS, .data = TOUCH_ANY, }, + .prompt = "You're ready to use the Pen brushes!\nNow, let's try a different brush. Press and hold the TOUCH PAD again..." + }, + { + .trigger = { .type = PRESS, .data = TOUCH_ANY | PB_RIGHT, }, + .backtrack = { .type = RELEASE, .data = TOUCH_ANY | SWIPE_LEFT | SWIPE_RIGHT | TOUCH_X | TOUCH_Y }, + .backtrackSteps = 1, + .prompt = "Then, press D-Pad RIGHT to change the brush..." + }, + { + .trigger = { .type = RELEASE, .data = TOUCH_ANY | TOUCH_X | TOUCH_Y | SWIPE_LEFT | SWIPE_RIGHT, }, + .prompt = STR_RELEASE_TOUCH_PAD_TO_CONFIRM }, + { + .trigger = { .type = CHANGE_BRUSH, .dataPtr = STR_BRUSH_RECTANGLE }, + .prompt = "Now, choose the RECTANGLE brush!" + }, + { + .trigger = { .type = RELEASE, .data = PB_A, }, + .backtrack = { .type = BRUSH_NOT, .dataPtr = STR_BRUSH_RECTANGLE }, + .backtrackSteps = 1, + .prompt = "Now, press A to select the first corner of the rectangle..." + }, + { + .trigger = { .type = PRESS_ANY, .data = (PB_UP | PB_DOWN | PB_LEFT | PB_RIGHT), }, + .backtrack = { .type = BRUSH_NOT, .dataPtr = STR_BRUSH_RECTANGLE }, + .backtrackSteps = 2, + .prompt = "Then move somewhere else..." + }, + { + .trigger = { .type = RELEASE, .data = PB_A, }, + .backtrack = { .type = BRUSH_NOT, .dataPtr = STR_BRUSH_RECTANGLE }, + .backtrackSteps = 3, + .prompt = "Press A again to pick the other coner of the rectangle. Note that the first point you picked will blink!" + }, + { + .trigger = { .type = CHANGE_BRUSH, .dataPtr = STR_BRUSH_POLYGON }, + .prompt = "Nice! Let's try out the POLYGON brush next." + }, + { + .trigger = { .type = RELEASE, .data = PB_A, }, + .backtrack = { .type = BRUSH_NOT, .dataPtr = STR_BRUSH_POLYGON }, + .backtrackSteps = 1, + .prompt = "Press A to select the first point of the polygon..." + }, + { + .trigger = { .type = RELEASE, .data = PB_A, }, + .backtrack = { .type = BRUSH_NOT, .dataPtr = STR_BRUSH_POLYGON }, + .backtrackSteps = 2, + .prompt = "Pick at least one more point for the polygon. Note that the first point will change color!" + }, + { + .trigger = { .type = DRAW_COMPLETE, }, + .backtrack = { .type = BRUSH_NOT, .dataPtr = STR_BRUSH_POLYGON }, + .backtrackSteps = 3, + .prompt = "To finish the polygon, connect it back to the original point, or use up all the remaining picks." + }, + { + .trigger = { .type = PRESS, .data = PB_START, }, + .prompt = "Good job! Now you know how to use all the brush types.\nNext, let's press START to toggle the menu" + }, + { + .trigger = { .type = PRESS_ANY, .data = PB_UP | PB_DOWN | PB_SELECT, }, + .backtrack = { .type = SELECT_MENU_ITEM, .data = HIDDEN }, + .backtrackSteps = 1, + .prompt = "Press UP, DOWN, or SELECT to go through the menu items" + }, + { + .trigger = { .type = SELECT_MENU_ITEM, .data = PICK_SLOT_SAVE, }, + .backtrack = { .type = SELECT_MENU_ITEM, .data = HIDDEN }, + .backtrackSteps = 2, + .prompt = "Great! Now, navigate to the SAVE option" + }, + { + .trigger = { .type = PRESS_ANY, .data = PB_LEFT | PB_RIGHT, }, + .backtrack = { .type = MENU_ITEM_NOT, .data = PICK_SLOT_SAVE }, + .backtrackSteps = 1, + .prompt = "Use D-Pad LEFT and RIGHT to switch between save slots here, or any other menu options" + }, + { + .trigger = { .type = PRESS_ANY, .data = PB_A | PB_B, }, + .backtrack = { .type = MENU_ITEM_NOT, .data = PICK_SLOT_SAVE }, + .backtrackSteps = 2, + .prompt = "Use the A BUTTON to confirm, or the B BUTTON to cancel and go back. You'll have to confirm again before deleting any art!" + }, + { + .trigger = { .type = SELECT_MENU_ITEM, .data = HIDDEN, }, + .prompt = "Press START or the B BUTTON to exit the menu" + }, + { + .trigger = { .type = PRESS, .data = PB_START, }, + .prompt = "Let's try editing the palette! Press START to open the menu one more time" + }, + { + .trigger = { .type = SELECT_MENU_ITEM, .data = EDIT_PALETTE, }, + .backtrack = { .type = SELECT_MENU_ITEM, .data = HIDDEN }, + .backtrackSteps = 1, + .prompt = "Use UP, DOWN, and SELECT to select the EDIT PALETTE menu item" + }, + { + .trigger = { .type = PRESS, .data = PB_A, }, + .backtrack = { .type = MENU_ITEM_NOT, .data = EDIT_PALETTE }, + .backtrackSteps = 1, + .prompt = "Press the A BUTTON to begin editing the palette" + }, + { + .trigger = { .type = PRESS_ANY, .data = PB_UP | PB_DOWN, }, + .backtrack = { .type = MODE_NOT, .data = BTN_MODE_PALETTE }, + .backtrackSteps = 2, + .prompt = "Use D-Pad UP and DOWN to select a color to edit" + }, + { + .trigger = { .type = PRESS_ANY, .data = PB_LEFT | PB_RIGHT, }, + .backtrack = { .type = MODE_NOT, .data = BTN_MODE_PALETTE }, + .backtrackSteps = 3, + .prompt = "Use D-Pad LEFT and RIGHT to switch between the RED, GREEN, and BLUE color sliders" + }, + { + .trigger = { .type = RELEASE, .data = TOUCH_ANY | PB_SELECT, }, + .backtrack = { .type = MODE_NOT, .data = BTN_MODE_PALETTE }, + .backtrackSteps = 4, + .prompt = "Tap along the TOUCH PAD to edit the selected color slider. You can also use SELECT" + }, + { + .trigger = { .type = PRESS_ANY, .data = PB_A | PB_B, }, + .backtrack = { .type = MODE_NOT, .data = BTN_MODE_PALETTE }, + .backtrackSteps = 5, + .prompt = "Press the A BUTTON to confirm and swap to the new color. Or, press the B BUTTON to restore the original color." + }, + { + .trigger = { .type = NO_TRIGGER, }, + .prompt = "That's everything.\nHappy painting!" + }, +}; + +const paintHelpStep_t* lastHelp = helpSteps + sizeof(helpSteps) / sizeof(helpSteps[0]) - 1; diff --git a/main/modes/mfpaint/paint_help.h b/main/modes/mfpaint/paint_help.h new file mode 100644 index 000000000..63329c452 --- /dev/null +++ b/main/modes/mfpaint/paint_help.h @@ -0,0 +1,114 @@ +#ifndef _PAINT_HELP_H_ +#define _PAINT_HELP_H_ + +#include "paint_common.h" +#include +#include + +#include "paint_type.h" + +typedef enum +{ + TOUCH_ANY = 0x0100, + TOUCH_X = 0x0200, + TOUCH_Y = 0x0400, + SWIPE_LEFT = 0x0800, + SWIPE_RIGHT = 0x1000, +} virtualButton_t; + +// Triggers for advancing the tutorial step +typedef enum +{ + NO_TRIGGER, + + // Press all of the given buttons at least once, in any order + PRESS_ALL, + + // Press any one of the give buttons + PRESS_ANY, + + // Press all of the given buttons at once + PRESS, + + // Release the given button + RELEASE, + + // A tool has drawn (not just picked a point) + DRAW_COMPLETE, + + CHANGE_BRUSH, + SELECT_MENU_ITEM, + CHANGE_MODE, + + // Inverse of CHANGE_BRUSH + BRUSH_NOT, + + // Inverse of SELECT_MENU_ITEM + MENU_ITEM_NOT, + + // Inverse of CHANGE_MODE + MODE_NOT, +} paintHelpTriggerType_t; + +typedef struct +{ + paintHelpTriggerType_t type; + const void* dataPtr; + int64_t data; +} paintHelpTrigger_t; + +typedef enum +{ + IND_NONE, + IND_BOX, + IND_ARROW, +} paintHelpIndicatorType_t; + +typedef struct +{ + paintHelpIndicatorType_t type; + + union + { + struct + { + uint16_t x0, y0, x1, y1; + } box; + struct + { + uint16_t x, y; + int dir; + } arrow; + }; +} paintHelpIndicator_t; + +typedef struct +{ + paintHelpTrigger_t trigger; + + // If this trigger is met, go back to a previous step + paintHelpTrigger_t backtrack; + + // The number of steps to go back if the backtrack trigger is met + uint8_t backtrackSteps; + + const char* prompt; +} paintHelpStep_t; + +typedef struct +{ + const paintHelpStep_t* curHelp; + uint16_t allButtons; + uint16_t curButtons; + uint16_t lastButton; + bool lastButtonDown; + + bool drawComplete; + + uint16_t helpH; +} paintHelp_t; + +extern const paintHelpStep_t helpSteps[]; +extern const paintHelpStep_t* lastHelp; + +#endif diff --git a/main/modes/mfpaint/paint_nvs.c b/main/modes/mfpaint/paint_nvs.c new file mode 100644 index 000000000..c1b3bb73d --- /dev/null +++ b/main/modes/mfpaint/paint_nvs.c @@ -0,0 +1,623 @@ +#include "paint_nvs.h" + +#include +#include +#include +#include + +#include "settingsManager.h" +#include "hdw-nvs.h" +#include "macros.h" + +#include "paint_common.h" +#include "paint_draw.h" +#include "paint_ui.h" +#include "paint_util.h" + +#define PAINT_PARAM(m, x, k, d) \ + static const settingParam_t paint##k##Param = {.min = m, .max = x, .def = d, .key = "mfp." #k} + +#define PAINT_BOOL_PARAM(k, d) PAINT_PARAM((0), (1), k, d) + +#define PAINT_BIT_PARAM(k) PAINT_PARAM(INT32_MIN, INT32_MAX, k, 0) + +static const char KEY_PAINT_INDEX[] = "pnt_idx"; +static const char KEY_PAINT_SLOT_PALETTE[] = "paint_%02" PRIu8 "_pal"; +static const char KEY_PAINT_SLOT_DIM[] = "paint_%02" PRIu8 "_dim"; +static const char KEY_PAINT_SLOT_CHUNK[] = "paint_%02" PRIu8 "c%05" PRIu32; + +/*static const settingParam_t paintOldIndex = { + .min = INT32_MIN, + .max = INT32_MAX, + .def = 0, + .key = KEY_PAINT_INDEX, +};*/ + +PAINT_BIT_PARAM(InUseSlot); +PAINT_BIT_PARAM(RecentSlot); +PAINT_BOOL_PARAM(EnableLeds, true); +PAINT_BOOL_PARAM(EnableBlink, true); +PAINT_BOOL_PARAM(Migrated, false); + +static void migrateIndex(int32_t index); +static void migrateSlot(uint8_t slot); +static int32_t paintReadParam(const settingParam_t* param); +static bool paintWriteParam(const settingParam_t* param, int32_t val); + +// void paintDebugIndex(int32_t index) +// { +// char bintext[33]; + +// for (uint8_t i = 0; i < 32; i++) +// { +// bintext[31 - i] = (index & (1 << i)) ? '1' : '0'; +// } + +// bintext[32] = '\0'; + +// PAINT_LOGD("PAINT INDEX:"); +// PAINT_LOGD(" Raw: %s", bintext); +// PAINT_LOGD(" Slots:"); +// PAINT_LOGD(" - [%c] Slot 1", paintGetSlotInUse(index, 0) ? 'X' : ' '); +// PAINT_LOGD(" - [%c] Slot 2", paintGetSlotInUse(index, 1) ? 'X' : ' '); +// PAINT_LOGD(" - [%c] Slot 3", paintGetSlotInUse(index, 2) ? 'X' : ' '); +// PAINT_LOGD(" - [%c] Slot 4", paintGetSlotInUse(index, 3) ? 'X' : ' '); +// if (paintGetRecentSlot(index) == PAINT_SAVE_SLOTS) +// { +// PAINT_LOGD(" Recent Slot: None"); +// } +// else +// { +// PAINT_LOGD(" Recent Slot: %d", paintGetRecentSlot(index) + 1); +// } +// PAINT_LOGD(" LEDs: %s", (index & PAINT_ENABLE_LEDS) ? "Yes" : "No"); +// PAINT_LOGD(" Blink: %s", (index & PAINT_ENABLE_BLINK) ? "Yes" : "No"); +// PAINT_LOGD("==========="); +// } + +const settingParam_t* paintGetInUseSlotBounds(void) +{ + return &paintInUseSlotParam; +} + +const settingParam_t* paintGetRecentSlotBounds(void) +{ + return &paintRecentSlotParam; +} + +const settingParam_t* paintGetEnableLedsBounds(void) +{ + return &paintEnableLedsParam; +} + +const settingParam_t* paintGetEnableBlinkBounds(void) +{ + return &paintEnableBlinkParam; +} + +const settingParam_t* paintGetMigratedBounds(void) +{ + return &paintMigratedParam; +} + +int32_t paintGetInUseSlots(void) +{ + return paintReadParam(&paintInUseSlotParam); +} + +void paintSetInUseSlots(int32_t inUseSlots) +{ + paintWriteParam(&paintInUseSlotParam, inUseSlots); +} + +/*int32_t paintGetRecentSlot(void) +{ + +}*/ + +/*void paintSetRecentSlot(int32_t recentSlot) +{ + +}*/ + +bool paintGetEnableLeds(void) +{ + return paintReadParam(&paintEnableLedsParam); +} + +void paintSetEnableLeds(bool enableLeds) +{ + paintWriteParam(&paintEnableLedsParam, enableLeds); +} + +bool paintGetEnableBlink(void) +{ + return paintReadParam(&paintEnableBlinkParam); +} + +void paintSetEnableBlink(bool enableBlink) +{ + paintWriteParam(&paintEnableBlinkParam, enableBlink); +} + +void migrateIndex(int32_t index) +{ + // TODO: Migrate the index to separate bitfields/bools per setting + // Sure, it wastes a few bytes, but... ok + // (32 bits for files in use / recent) + // 1. Check if the index has already been migrated + // 2. Read each bitfield within the old index + // 3. Write each value to its new setting + // 4. Update the index value to indicate it was migrated + // 5. Save the index + // 6. Never touch it again + + // if (!(index & PAINT_INDEX_MIGRATED)) { + // Index has not been migrated, do it here + + // index |= PAINT_INDEX_MIGRATED; + //} +} + +void migrateSlot(uint8_t slot) +{ + // TODO: Migrate slots to a compressed storage format / one that just uses a single chunk + // blobs can be much bigger than the size of a max image, but there should probably be a + // somewhat robust check to make sure we don't go below some threshold of available space. + // Compression -- check if we can use miniz.h in here (don't see why not). +} + +static int32_t paintReadParam(const settingParam_t* param) +{ + int32_t out; + // Wrap this just in case we need to do stuff later + if (!readNvs32(param->key, &out)) + { + // Set the default value if the read failed + out = param->def; + } + + return out; +} + +static bool paintWriteParam(const settingParam_t* param, int32_t val) +{ + return writeNvs32(param->key, CLAMP(val, param->min, param->max)); +} + +void paintLoadIndex(int32_t* index) +{ + // SFX? + // BGM | Lights? + // \|/ + // |xxxxxvvv |Recent?| |Inuse? | + // 0000 0000 0000 0000 0000 0000 + + // TODO: First, try to read our new index, and if that fails, see if the old index is around and migrate that + + // if (paintReadParam(&index)) + if (!readNvs32(KEY_PAINT_INDEX, index)) + { + PAINT_LOGW("No metadata! Setting defaults"); + *index = PAINT_DEFAULTS; + paintSaveIndex(*index); + } +} + +void paintSaveIndex(int32_t index) +{ + if (writeNvs32(KEY_PAINT_INDEX, index)) + { + PAINT_LOGD("Saved index: %04" PRIx32, index); + } + else + { + PAINT_LOGE("Failed to save index :("); + } +} + +// void paintResetStorage(int32_t* index) +// { +// *index = PAINT_DEFAULTS; +// paintSaveIndex(*index); +// } + +bool paintGetSlotInUse(int32_t index, uint8_t slot) +{ + return (index & (1 << slot)) != 0; +} + +void paintClearSlot(int32_t* index, uint8_t slot) +{ + *index &= ~(1 << slot); + paintSaveIndex(*index); +} + +void paintSetSlotInUse(int32_t* index, uint8_t slot) +{ + *index |= (1 << slot); + paintSaveIndex(*index); +} + +bool paintGetAnySlotInUse(int32_t index) +{ + return (index & ((1 << PAINT_SAVE_SLOTS) - 1)) != 0; +} + +/** + * Returns the most recent save slot used, or PAINT_SAVE_SLOTS if there is none set + */ +uint8_t paintGetRecentSlot(int32_t index) +{ + return (index >> PAINT_SAVE_SLOTS) & 0b111; +} + +void paintSetRecentSlot(int32_t* index, uint8_t slot) +{ + // TODO if we change the number of slots this will totally not work anymore + // I mean, we could just do & 0xFF and waste 5 whole bits + *index = (*index & PAINT_MASK_NOT_RECENT) | ((slot & 0b111) << PAINT_SAVE_SLOTS); + paintSaveIndex(*index); +} + +/** + * Returns the number of bytes needed to store the image pixel data + */ +size_t paintGetStoredSize(const paintCanvas_t* canvas) +{ + return (canvas->w * canvas->h + 1) / 2; +} + +bool paintDeserialize(paintCanvas_t* dest, const uint8_t* data, size_t offset, size_t count) +{ + uint16_t x0, y0, x1, y1; + for (uint16_t n = 0; n < count; n++) + { + if (offset * 2 + (n * 2) >= dest->w * dest->h) + { + // If we've just read the last pixel, exit early + return false; + } + + // no need for logic to exit the final chunk early, since each chunk's size is given to us + // calculate the canvas coordinates given the pixel indices + x0 = (offset * 2 + (n * 2)) % dest->w; + y0 = (offset * 2 + (n * 2)) / dest->w; + x1 = (offset * 2 + (n * 2 + 1)) % dest->w; + y1 = (offset * 2 + (n * 2 + 1)) / dest->w; + + setPxScaled(x0, y0, dest->palette[data[n] >> 4], dest->x, dest->y, dest->xScale, dest->yScale); + setPxScaled(x1, y1, dest->palette[data[n] & 0xF], dest->x, dest->y, dest->xScale, dest->yScale); + } + + return offset * 2 + (count * 2) < dest->w * dest->h; +} + +size_t paintSerialize(uint8_t* dest, const paintCanvas_t* canvas, size_t offset, size_t count) +{ + uint8_t paletteIndex[cTransparent + 1]; + + // Build the reverse-palette map + for (uint16_t i = 0; i < PAINT_MAX_COLORS; i++) + { + paletteIndex[((uint8_t)canvas->palette[i])] = i; + } + + uint16_t x0, y0, x1, y1; + // build the chunk + for (uint16_t n = 0; n < count; n++) + { + if (offset * 2 + (n * 2) >= canvas->w * canvas->h) + { + // If we've just stored the last pixel, return false to indicate that we're done + return n; + } + + // calculate the real coordinates given the pixel indices + // (we store 2 pixels in each byte) + // that's 100% more pixel, per pixel! + x0 = canvas->x + (offset * 2 + (n * 2)) % canvas->w * canvas->xScale; + y0 = canvas->y + (offset * 2 + (n * 2)) / canvas->w * canvas->yScale; + x1 = canvas->x + (offset * 2 + (n * 2 + 1)) % canvas->w * canvas->xScale; + y1 = canvas->y + (offset * 2 + (n * 2 + 1)) / canvas->w * canvas->yScale; + + // we only need to save the top-left pixel of each scaled pixel, since they're the same unless something is very + // broken + dest[n] = paletteIndex[(uint8_t)getPxTft(x0, y0)] << 4 | paletteIndex[(uint8_t)getPxTft(x1, y1)]; + } + + return count; +} + +bool paintSave(int32_t* index, const paintCanvas_t* canvas, uint8_t slot) +{ + // NVS blob key name + char key[16]; + + // pointer to converted image segment + uint8_t* imgChunk = NULL; + + if ((imgChunk = malloc(sizeof(uint8_t) * PAINT_SAVE_CHUNK_SIZE)) != NULL) + { + PAINT_LOGD("Allocated %d bytes for image chunk", PAINT_SAVE_CHUNK_SIZE); + } + else + { + PAINT_LOGE("malloc failed for %d bytes", PAINT_SAVE_CHUNK_SIZE); + return false; + } + + // Save the palette map, this lets us compact the image by 50% + snprintf(key, 16, KEY_PAINT_SLOT_PALETTE, slot); + PAINT_LOGD("paletteColor_t size: %" PRIu32 ", max colors: %d", (uint32_t)sizeof(paletteColor_t), PAINT_MAX_COLORS); + PAINT_LOGD("Palette will take up %" PRIu32 " bytes", (uint32_t)sizeof(canvas->palette)); + if (writeNvsBlob(key, canvas->palette, sizeof(canvas->palette))) + { + PAINT_LOGD("Saved palette to slot %s", key); + } + else + { + PAINT_LOGE("Could't save palette to slot %s", key); + free(imgChunk); + return false; + } + + // Save the canvas dimensions + uint32_t packedSize = canvas->w << 16 | canvas->h; + snprintf(key, 16, KEY_PAINT_SLOT_DIM, slot); + if (writeNvs32(key, packedSize)) + { + PAINT_LOGD("Saved dimensions to slot %s", key); + } + else + { + PAINT_LOGE("Couldn't save dimensions to slot %s", key); + free(imgChunk); + return false; + } + + size_t offset = 0; + size_t written = 0; + uint32_t chunkNumber = 0; + + // Write until we're done + while (0 != (written = paintSerialize(imgChunk, canvas, offset, PAINT_SAVE_CHUNK_SIZE))) + { + // save the chunk + snprintf(key, 16, KEY_PAINT_SLOT_CHUNK, slot, chunkNumber); + if (writeNvsBlob(key, imgChunk, written)) + { + PAINT_LOGD("Saved blob %" PRIu32 " with %" PRIu32 " bytes", chunkNumber, (uint32_t)written); + } + else + { + PAINT_LOGE("Unable to save blob %" PRIu32, chunkNumber); + free(imgChunk); + return false; + } + + offset += written; + chunkNumber++; + } + + paintSetSlotInUse(index, slot); + paintSetRecentSlot(index, slot); + paintSaveIndex(*index); + + free(imgChunk); + imgChunk = NULL; + + return true; +} + +bool paintLoad(int32_t* index, paintCanvas_t* canvas, uint8_t slot) +{ + // NVS blob key name + char key[16]; + + // pointer to converted image segment + uint8_t* imgChunk = NULL; + + // Allocate space for the chunk + if ((imgChunk = malloc(PAINT_SAVE_CHUNK_SIZE)) != NULL) + { + PAINT_LOGD("Allocated %d bytes for image chunk", PAINT_SAVE_CHUNK_SIZE); + } + else + { + PAINT_LOGE("malloc failed for %d bytes", PAINT_SAVE_CHUNK_SIZE); + return false; + } + + // read the palette and load it into the recentColors + // read the dimensions and do the math + // read the pixels + + size_t paletteSize; + + if (!paintGetSlotInUse(*index, slot)) + { + PAINT_LOGW("Attempted to load from uninitialized slot %" PRIu8, slot); + free(imgChunk); + return false; + } + + // Load the palette map + snprintf(key, 16, KEY_PAINT_SLOT_PALETTE, slot); + + if (!readNvsBlob(key, NULL, &paletteSize)) + { + PAINT_LOGE("Couldn't read size of palette in slot %s", key); + free(imgChunk); + return false; + } + + // TODO Move this outside of this function + if (readNvsBlob(key, canvas->palette, &paletteSize)) + { + PAINT_LOGD("Read %" PRIu32 " bytes of palette from slot %s", (uint32_t)paletteSize, key); + } + else + { + PAINT_LOGE("Could't read palette from slot %s", key); + free(imgChunk); + return false; + } + + if (!paintLoadDimensions(canvas, slot)) + { + PAINT_LOGE("Slot %" PRIu8 " has 0 dimension! Stopping load and clearing slot", slot); + paintClearSlot(index, slot); + free(imgChunk); + return false; + } + + // Read all the chunks + size_t lastChunkSize = 0; + uint32_t chunkNumber = 0; + size_t offset = 0; + + do + { + offset += lastChunkSize; + snprintf(key, 16, KEY_PAINT_SLOT_CHUNK, slot, chunkNumber); + // panic + if (!readNvsBlob(key, NULL, &lastChunkSize) || lastChunkSize > PAINT_SAVE_CHUNK_SIZE) + { + PAINT_LOGE("Unable to read size of blob %" PRIu32 " in slot %s", chunkNumber, key); + free(imgChunk); + return false; + } + + // read the chunk + if (readNvsBlob(key, imgChunk, &lastChunkSize)) + { + PAINT_LOGD("Read blob %" PRIu32 " (%" PRIu32 " bytes)", (uint32_t)chunkNumber, (uint32_t)lastChunkSize); + } + else + { + PAINT_LOGE("Unable to read blob %" PRIu32, chunkNumber); + // do panic if we miss one chunk, it's probably not ok... + free(imgChunk); + return false; + } + + chunkNumber++; + // paintDeserialize() will return true if there's more to be read + } while (paintDeserialize(canvas, imgChunk, offset, lastChunkSize)); + + free(imgChunk); + imgChunk = NULL; + + return true; +} + +bool paintLoadDimensions(paintCanvas_t* canvas, uint8_t slot) +{ + char key[16]; + // Read the canvas dimensions + PAINT_LOGD("Reading dimensions"); + int32_t packedSize; + snprintf(key, 16, KEY_PAINT_SLOT_DIM, slot); + if (readNvs32(key, &packedSize)) + { + canvas->h = (uint32_t)packedSize & 0xFFFF; + canvas->w = (((uint32_t)packedSize) >> 16) & 0xFFFF; + PAINT_LOGD("Read dimensions from slot %s: %d x %d", key, canvas->w, canvas->h); + + if (canvas->h == 0 || canvas->w == 0) + { + return false; + } + } + else + { + PAINT_LOGE("Couldn't read dimensions from slot %s", key); + return false; + } + + return true; +} + +uint8_t paintGetPrevSlotInUse(int32_t index, uint8_t slot) +{ + do + { + // Switch to the previous slot, wrapping back to the end + slot = PREV_WRAP(slot, PAINT_SAVE_SLOTS); + } + // If we're loading, and there's actually a slot we can load from, skip empty slots until we find one that is in use + while (paintGetAnySlotInUse(index) && !paintGetSlotInUse(index, slot)); + + return slot; +} + +uint8_t paintGetNextSlotInUse(int32_t index, uint8_t slot) +{ + do + { + slot = NEXT_WRAP(slot, PAINT_SAVE_SLOTS); + } while (paintGetAnySlotInUse(index) && !paintGetSlotInUse(index, slot)); + + return slot; +} + +void paintDeleteSlot(int32_t* index, uint8_t slot) +{ + // NVS blob key name + char key[16]; + + if (slot >= PAINT_SAVE_SLOTS) + { + PAINT_LOGE("Attempt to delete invalid slto %d", slot); + return; + } + + if (!paintGetSlotInUse(*index, slot)) + { + PAINT_LOGW("Attempting to delete allegedly unused slot %d", slot); + } + + // Delete the palette + snprintf(key, 16, KEY_PAINT_SLOT_PALETTE, slot); + if (!eraseNvsKey(key)) + { + PAINT_LOGE("Couldn't delete palette of slot %d", slot); + } + + snprintf(key, 16, KEY_PAINT_SLOT_DIM, slot); + if (!eraseNvsKey(key)) + { + PAINT_LOGE("Couldn't delete dimensions of slot %d", slot); + } + + // Erase chunks until we fail to find one + uint32_t i = 0; + do + { + snprintf(key, 16, KEY_PAINT_SLOT_CHUNK, slot, i++); + } while (eraseNvsKey(key)); + + PAINT_LOGI("Erased %" PRIu32 " chunks of slot %" PRIu8, i - 1, slot); + paintClearSlot(index, slot); + + if (paintGetRecentSlot(*index) == slot) + { + // Unset the most recent slot if we're deleting it + paintSetRecentSlot(index, PAINT_SAVE_SLOTS); + } +} + +bool paintDeleteIndex(void) +{ + if (eraseNvsKey(KEY_PAINT_INDEX)) + { + PAINT_LOGI("Erased index!"); + return true; + } + else + { + PAINT_LOGE("Failed to erase index!"); + return false; + } +} \ No newline at end of file diff --git a/main/modes/mfpaint/paint_nvs.h b/main/modes/mfpaint/paint_nvs.h new file mode 100644 index 000000000..e539bbac9 --- /dev/null +++ b/main/modes/mfpaint/paint_nvs.h @@ -0,0 +1,52 @@ +#ifndef _PAINT_NVS_H_ +#define _PAINT_NVS_H_ + +#include +#include +#include + +#include "settingsManager.h" +#include "hdw-nvs.h" + +#include "paint_common.h" +#include "paint_type.h" + +// Settings bounds for the menu +const settingParam_t* paintGetInUseSlotBounds(void); +const settingParam_t* paintGetRecentSlotBounds(void); +const settingParam_t* paintGetEnableLedsBounds(void); +const settingParam_t* paintGetEnableBlinkBounds(void); +const settingParam_t* paintGetMigratedBounds(void); + +// Getters / setters for the new separated values +int32_t paintGetInUseSlots(void); +void paintSetInUseSlots(int32_t inUseSlots); +// int32_t paintGetRecentSlot(void); +// void paintSetRecentSlot(int32_t recentSlot); +bool paintGetEnableLeds(void); +void paintSetEnableLeds(bool enableLeds); +bool paintGetEnableBlink(void); +void paintSetEnableBlink(bool enableBlink); + +// void paintDebugIndex(int32_t index); +void paintLoadIndex(int32_t* dest); +void paintSaveIndex(int32_t index); +// void paintResetStorage(int32_t* index); +bool paintGetSlotInUse(int32_t index, uint8_t slot); +void paintClearSlot(int32_t* index, uint8_t slot); +void paintSetSlotInUse(int32_t* index, uint8_t slot); +bool paintGetAnySlotInUse(int32_t index); +uint8_t paintGetRecentSlot(int32_t index); +void paintSetRecentSlot(int32_t* index, uint8_t slot); +size_t paintGetStoredSize(const paintCanvas_t* canvas); +bool paintDeserialize(paintCanvas_t* dest, const uint8_t* data, size_t offset, size_t count); +size_t paintSerialize(uint8_t* dest, const paintCanvas_t* canvas, size_t offset, size_t count); +bool paintSave(int32_t* index, const paintCanvas_t* canvas, uint8_t slot); +bool paintLoad(int32_t* index, paintCanvas_t* canvas, uint8_t slot); +bool paintLoadDimensions(paintCanvas_t* canvas, uint8_t slot); +uint8_t paintGetPrevSlotInUse(int32_t index, uint8_t slot); +uint8_t paintGetNextSlotInUse(int32_t index, uint8_t slot); +void paintDeleteSlot(int32_t* index, uint8_t slot); +bool paintDeleteIndex(void); + +#endif diff --git a/main/modes/mfpaint/paint_share.c b/main/modes/mfpaint/paint_share.c new file mode 100644 index 000000000..e970736b3 --- /dev/null +++ b/main/modes/mfpaint/paint_share.c @@ -0,0 +1,1333 @@ +#include "paint_share.h" + +#include +#include + +#include "p2pConnection.h" +#include "shapes.h" +#include "hdw-btn.h" + +#include "mode_paint.h" +#include "paint_common.h" +#include "paint_nvs.h" +#include "paint_util.h" + +/** + * Share mode! + * + * So, how will this work? + * On the SENDING swadge: + * - Select "Share" mode from the paint menu + * - The image from the most recent slot is displayed (at a smaller scale than in draw mode) + * - The user can page with Left and Right, or Select to switch between used slots + * - The user can begin sharing by pressing A or Start + * - Once sharing begins, the swadge opens a P2P connection + * - When a receiving swadge is found, sharing begins immediately. + * - The sender sends a metadata packet, which includes canvas dimensions, and palette + * - We wait for confirmation that the metadata was acked (TODO: and handled properly.) + * - Now, we begin sending packets. We wait until each one is ACKed before sending another (TODO: don't send another + * packet until the receiver asks for more) + * - Each packet contains an absolute sequence number and as many bytes of pixel data as will fit (palette-indexed and + * packed into 2 pixels per byte) + * - Once the last packet has been acked, we're done! Return to share mode + */ + +#define SHARE_LEFT_MARGIN 10 +#define SHARE_RIGHT_MARGIN 10 +#define SHARE_TOP_MARGIN 25 +#define SHARE_BOTTOM_MARGIN 25 + +#define SHARE_PROGRESS_LEFT 30 +#define SHARE_PROGESS_RIGHT 30 + +#define SHARE_PROGRESS_SPEED 25000 + +// Reset after 5 seconds without a packet +#define CONN_LOST_TIMEOUT 5000000 + +#define SHARE_BG_COLOR c444 +#define SHARE_CANVAS_BORDER c000 +#define SHARE_PROGRESS_BORDER c000 +#define SHARE_PROGRESS_BG c555 +#define SHARE_PROGRESS_FG c350 + +// Uncomment to display extra connection debugging info on screen +// #define SHARE_NET_DEBUG + +const uint8_t SHARE_PACKET_CANVAS_DATA = 0; +const uint8_t SHARE_PACKET_PIXEL_DATA = 1; +const uint8_t SHARE_PACKET_PIXEL_REQUEST = 2; +const uint8_t SHARE_PACKET_RECEIVE_COMPLETE = 3; +const uint8_t SHARE_PACKET_ABORT = 4; + +// The canvas data packet has PAINT_MAX_COLORS bytes of palette, plus 2 uint16_ts of width/height. Also 1 for size +const uint8_t PACKET_LEN_CANVAS_DATA = sizeof(uint8_t) * PAINT_MAX_COLORS + sizeof(uint16_t) * 2; + +static const char strOverwriteSlot[] = "Overwrite Slot %d"; +static const char strEmptySlot[] = "Save in Slot %d"; +static const char strShareSlot[] = "Share Slot %d"; +static const char strControlsShare[] = "A to Share"; +static const char strControlsSave[] = "A to Save"; +static const char strControlsCancel[] = "B to Cancel"; + +paintShare_t* paintShare; + +void paintShareCommonSetup(void); +void paintShareEnterMode(void); +void paintReceiveEnterMode(void); +void paintShareExitMode(void); +void paintShareMainLoop(int64_t elapsedUs); +void paintShareButtonCb(buttonEvt_t* evt); +void paintShareRecvCb(const esp_now_recv_info_t* esp_now_info, const uint8_t* data, uint8_t len, int8_t rssi); +void paintShareSendCb(const uint8_t* mac_addr, esp_now_send_status_t status); + +void paintShareP2pConnCb(p2pInfo* p2p, connectionEvt_t evt); +void paintShareP2pSendCb(p2pInfo* p2p, messageStatus_t status, const uint8_t* data, uint8_t len); +void paintShareP2pMsgRecvCb(p2pInfo* p2p, const uint8_t* payload, uint8_t len); + +void paintShareRenderProgressBar(int64_t elapsedUs, uint16_t x, uint16_t y, uint16_t w, uint16_t h); +void paintRenderShareMode(int64_t elapsedUs); + +void paintBeginShare(void); + +void paintShareInitP2p(void); +void paintShareDeinitP2p(void); + +void paintShareMsgSendOk(void); +void paintShareMsgSendFail(void); + +void paintShareSendPixelRequest(void); +void paintShareSendReceiveComplete(void); +// void paintShareSendAbort(void); + +void paintShareSendCanvas(void); +void paintShareHandleCanvas(void); + +void paintShareSendPixels(void); +void paintShareHandlePixels(void); + +void paintShareCheckForTimeout(void); +void paintShareRetry(void); + +void paintShareDoLoad(void); +void paintShareDoSave(void); + +#ifdef SHARE_NET_DEBUG + +const char* paintShareStateToStr(paintShareState_t state); + +const char* paintShareStateToStr(paintShareState_t state) +{ + switch (state) + { + case SHARE_SEND_SELECT_SLOT: + return "SEL_SLOT"; + + case SHARE_SEND_WAIT_FOR_CONN: + return "S_W_CON"; + + case SHARE_RECV_WAIT_FOR_CONN: + return "R_W_CON"; + + case SHARE_RECV_WAIT_CANVAS_DATA: + return "R_W_CNV"; + + case SHARE_SEND_CANVAS_DATA: + return "S_S_CNV"; + + case SHARE_SEND_WAIT_CANVAS_DATA_ACK: + return "S_W_CNV_ACK"; + + case SHARE_SEND_WAIT_FOR_PIXEL_REQUEST: + return "S_W_PXRQ"; + + case SHARE_SEND_PIXEL_DATA: + return "S_S_PX"; + + case SHARE_RECV_PIXEL_DATA: + return "R_R_PX"; + + case SHARE_SEND_WAIT_PIXEL_DATA_ACK: + return "S_W_PX_ACK"; + + case SHARE_RECV_SELECT_SLOT: + return "SEL_SLOT"; + + case SHARE_SEND_COMPLETE: + return "DONE"; + + default: + return "?????"; + } +} + +bool paintShareLogState(char* dest, size_t size); + +bool paintShareLogState(char* dest, size_t size) +{ + // initialize to invalid value + static paintShareState_t _lastState = 12; + if (_lastState == paintShare->shareState) + { + return false; + } + + snprintf(dest, size, "%s->%s", paintShareStateToStr(_lastState), paintShareStateToStr(paintShare->shareState)); + + _lastState = paintShare->shareState; + + return true; +} +#endif + +// Use a different swadge mode so the main game doesn't take as much battery +swadgeMode_t modePaintShare = { + .modeName = "MFPaint.net Send", + .wifiMode = ESP_NOW, + .overrideUsb = false, + .usesAccelerometer = false, + .usesThermometer = false, + .fnEnterMode = paintShareEnterMode, + .fnExitMode = paintShareExitMode, + .fnMainLoop = paintShareMainLoop, + .fnAudioCallback = NULL, + .fnBackgroundDrawCallback = NULL, + .fnEspNowRecvCb = paintShareRecvCb, + .fnEspNowSendCb = paintShareSendCb, + .fnAdvancedUSB = NULL, +}; + +swadgeMode_t modePaintReceive = { + .modeName = "MFPaint.net Recv", + .wifiMode = ESP_NOW, + .overrideUsb = false, + .usesAccelerometer = false, + .usesThermometer = false, + .fnEnterMode = paintReceiveEnterMode, + .fnExitMode = paintShareExitMode, + .fnMainLoop = paintShareMainLoop, + .fnAudioCallback = NULL, + .fnEspNowRecvCb = paintShareRecvCb, + .fnEspNowSendCb = paintShareSendCb, + .fnAdvancedUSB = NULL, +}; + +static bool isSender(void) +{ + return paintShare->isSender; +} + +void paintShareInitP2p(void) +{ + paintShare->connectionStarted = true; + paintShare->shareSeqNum = 0; + paintShare->shareNewPacket = false; + + p2pDeinit(&paintShare->p2pInfo); + p2pInitialize(&paintShare->p2pInfo, isSender() ? 'P' : 'Q', paintShareP2pConnCb, paintShareP2pMsgRecvCb, -35); + p2pSetAsymmetric(&paintShare->p2pInfo, isSender() ? 'Q' : 'P'); + p2pStartConnection(&paintShare->p2pInfo); +} + +void paintShareDeinitP2p(void) +{ + paintShare->connectionStarted = false; + p2pDeinit(&paintShare->p2pInfo); + + paintShare->shareSeqNum = 0; + paintShare->shareNewPacket = false; +} + +void paintShareCommonSetup(void) +{ + paintShare = calloc(1, sizeof(paintShare_t)); + + PAINT_LOGD("Entering Share Mode"); + paintLoadIndex(&paintShare->index); + + paintShare->connectionStarted = false; + + if (!loadFont(PAINT_SHARE_TOOLBAR_FONT, &paintShare->toolbarFont, false)) + { + PAINT_LOGE("Unable to load font!"); + } + + if (!loadWsg("arrow12.wsg", &paintShare->arrowWsg, false)) + { + PAINT_LOGE("Unable to load arrow WSG!"); + } + else + { + // Recolor the arrow to black + colorReplaceWsg(&paintShare->arrowWsg, c555, c000); + } + + // Set the display + paintShare->shareNewPacket = false; + paintShare->shareUpdateScreen = true; + paintShare->shareTime = 0; +} + +void paintReceiveEnterMode(void) +{ + paintShareCommonSetup(); + paintShare->isSender = false; + + PAINT_LOGD("Receiver: Waiting for connection"); + paintShare->shareState = SHARE_RECV_WAIT_FOR_CONN; +} + +void paintShareEnterMode(void) +{ + paintShareCommonSetup(); + paintShare->isSender = true; + + //////// Load an image... + + PAINT_LOGD("Sender: Selecting slot"); + paintShare->shareState = SHARE_SEND_SELECT_SLOT; + + if (!paintGetAnySlotInUse(paintShare->index)) + { + PAINT_LOGE("Share mode started without any saved images. Exiting"); + switchToSwadgeMode(&modePaint); + return; + } + + // Start on the most recently saved slot + paintShare->shareSaveSlot = paintGetRecentSlot(paintShare->index); + if (paintShare->shareSaveSlot == PAINT_SAVE_SLOTS) + { + // If there was no recently saved slot, find the first slot in use instead + paintShare->shareSaveSlot = paintGetNextSlotInUse(paintShare->index, PAINT_SAVE_SLOTS - 1); + } + + PAINT_LOGD("paintShare->shareSaveSlot = %d", paintShare->shareSaveSlot); + + paintShare->clearScreen = true; +} + +void paintShareSendCb(const uint8_t* mac_addr, esp_now_send_status_t status) +{ + p2pSendCb(&paintShare->p2pInfo, mac_addr, status); +} + +void paintShareRenderProgressBar(int64_t elapsedUs, uint16_t x, uint16_t y, uint16_t w, uint16_t h) +{ + // okay, we're gonna have a real progress bar, not one of those lying fake progress bars + // 1. While waiting to connect, draw an indeterminate progress bar, like [|| || || ||] -> [ || || || |] -> [ + // || || || ] + // 2. Once connected, we use an absolute progress bar. Basically it's out of the total number of bytes + // 3. Canvas data: Ok, we'll treat each packet as though it's the same size. After sending, we add 2 to the + // progress. After ACK, we add 1. After pixel data request, we add another (or is that just the next one) + // 4. Pixel data: 2 for send, +1 for ack, +1 for receiving + + bool indeterminate = false; + uint16_t progress = 0; + + switch (paintShare->shareState) + { + case SHARE_SEND_SELECT_SLOT: + // No draw + return; + + case SHARE_SEND_WAIT_FOR_CONN: + case SHARE_RECV_WAIT_FOR_CONN: + case SHARE_RECV_WAIT_CANVAS_DATA: + // We're not connected yet, or don't yet know how much data to expect + indeterminate = true; + break; + + case SHARE_SEND_CANVAS_DATA: + // Haven't sent anything yet, progress at 0 + progress = 0; + break; + + case SHARE_SEND_WAIT_CANVAS_DATA_ACK: + // Sent the canvas data, progress at 2 + progress = 2; + break; + + case SHARE_SEND_WAIT_FOR_PIXEL_REQUEST: + // Sent (canvas or pixel data, depending on seqnum), progress at 4*(seqnum+1) + 3 + progress = 4 * (paintShare->shareSeqNum + 1) + 3; + break; + + case SHARE_SEND_PIXEL_DATA: + case SHARE_RECV_PIXEL_DATA: + // WWaiting to send or receive pixel data, progress at 4*(seqnum+1) + 0 + progress = 4 * (paintShare->shareSeqNum + 1); + break; + + case SHARE_SEND_WAIT_PIXEL_DATA_ACK: + // Sent pixel data, progress at 4*(seqnum+1) + 2 + progress = 4 * (paintShare->shareSeqNum + 1) + 2; + break; + + case SHARE_RECV_SELECT_SLOT: + case SHARE_SEND_COMPLETE: + // We're done, draw the whole progress bar for fun + progress = 0xFFFF; + break; + } + + // Draw border + drawRect(x, y, x + w, y + h, SHARE_PROGRESS_BORDER); + drawRectFilled(x + 1, y + 1, x + w, y + h, SHARE_PROGRESS_BG); + + if (!indeterminate) + { + paintShare->shareTime = 0; + // if we have the canvas dimensinos, we can calculate the max progress + // (WIDTH / ((totalPacketNum + 1) * 4)) * (currentPacketNum) * 4 + (sent: 2, acked: 3, req'd: 4) + uint16_t maxProgress + = ((paintShare->canvas.h * paintShare->canvas.w + PAINT_SHARE_PX_PER_PACKET - 1) / PAINT_SHARE_PX_PER_PACKET + + 1); + + // Now, we just draw a box at (progress * (width) / maxProgress) + uint16_t size = (progress > maxProgress ? maxProgress : progress) * w / maxProgress; + drawRectFilled(x + 1, y + 1, x + size, y + h, SHARE_PROGRESS_FG); + } + else + { + uint8_t segCount = 4; + uint8_t segW = ((w - 2) / segCount / 2); + uint8_t offset = (elapsedUs / SHARE_PROGRESS_SPEED) % ((w - 2) / segCount); + + for (uint8_t i = 0; i < segCount; i++) + { + uint16_t x0 = (offset + i * (segW * 2)) % (w - 2); + uint16_t x1 = (offset + i * (segW * 2) + segW) % (w - 2); + + if (x0 >= x1) + { + // Split the segment into two parts + // From x0 to MAX + drawRectFilled(x + 1 + x0, y + 1, x + w, y + h, SHARE_PROGRESS_FG); + + if (x1 != 0) + { + // From 0 to x1 + // Don't draw this if x1 == 0, because then x + 1 == x + 1 + x1, and there would be no box + drawRectFilled(x + 1, y + 1, x + 1 + x1, y + h, SHARE_PROGRESS_FG); + } + } + else + { + drawRectFilled(x + 1 + x0, y + 1, x + 1 + x1, y + h, SHARE_PROGRESS_FG); + } + } + } +} + +void paintRenderShareMode(int64_t elapsedUs) +{ + if (paintShare->canvas.h != 0 && paintShare->canvas.w != 0) + { + // Top part of screen + fillDisplayArea(0, 0, TFT_WIDTH, paintShare->canvas.y, SHARE_BG_COLOR); + + // Left side of screen + fillDisplayArea(0, 0, paintShare->canvas.x, TFT_HEIGHT, SHARE_BG_COLOR); + + // Right side of screen + fillDisplayArea(paintShare->canvas.x + paintShare->canvas.w * paintShare->canvas.xScale, 0, TFT_WIDTH, + TFT_HEIGHT, SHARE_BG_COLOR); + + // Bottom of screen + fillDisplayArea(0, paintShare->canvas.y + paintShare->canvas.h * paintShare->canvas.yScale, TFT_WIDTH, + TFT_HEIGHT, SHARE_BG_COLOR); + + // Border the canvas + drawRect(paintShare->canvas.x - 1, paintShare->canvas.y - 1, + paintShare->canvas.x + paintShare->canvas.w * paintShare->canvas.xScale + 1, + paintShare->canvas.y + paintShare->canvas.h * paintShare->canvas.yScale + 1, SHARE_CANVAS_BORDER); + } + else + { + // There's no canvas, so just... clear everything + fillDisplayArea(0, 0, TFT_WIDTH, TFT_HEIGHT, SHARE_BG_COLOR); + } + + char text[32]; + const char* bottomText = NULL; + bool arrows = false; + + switch (paintShare->shareState) + { + case SHARE_RECV_SELECT_SLOT: + { + arrows = true; + bottomText = strControlsSave; + snprintf(text, sizeof(text), + paintGetSlotInUse(paintShare->index, paintShare->shareSaveSlot) ? strOverwriteSlot : strEmptySlot, + paintShare->shareSaveSlot + 1); + break; + } + case SHARE_SEND_SELECT_SLOT: + { + arrows = true; + bottomText = strControlsShare; + snprintf(text, sizeof(text), strShareSlot, paintShare->shareSaveSlot + 1); + break; + } + case SHARE_SEND_WAIT_FOR_CONN: + case SHARE_RECV_WAIT_FOR_CONN: + { + bottomText = strControlsCancel; + snprintf(text, sizeof(text), "Connecting..."); + break; + } + + case SHARE_SEND_CANVAS_DATA: + case SHARE_SEND_WAIT_CANVAS_DATA_ACK: + case SHARE_SEND_WAIT_FOR_PIXEL_REQUEST: + case SHARE_SEND_PIXEL_DATA: + case SHARE_SEND_WAIT_PIXEL_DATA_ACK: + { + snprintf(text, sizeof(text), "Sending..."); + break; + } + + case SHARE_SEND_COMPLETE: + { + snprintf(text, sizeof(text), "Complete"); + break; + } + + case SHARE_RECV_WAIT_CANVAS_DATA: + case SHARE_RECV_PIXEL_DATA: + { + snprintf(text, sizeof(text), "Receiving..."); + break; + } + } + +#ifdef SHARE_NET_DEBUG + static char debugText[32] = {{0}}; + paintShareLogState(debugText, sizeof(debugText)); + bottomText = debugText; +#endif + + // debug lines + // drawLine(paintShare->disp, 0, SHARE_TOP_MARGIN, paintShare->disp->w, SHARE_TOP_MARGIN, c000, 2); + // drawLine(paintShare->disp, 0, paintShare->disp->h - SHARE_BOTTOM_MARGIN, paintShare->disp->w, paintShare->disp->h + // - SHARE_BOTTOM_MARGIN, c000, 2); + + paintShareRenderProgressBar(elapsedUs, SHARE_PROGRESS_LEFT, 0, + TFT_WIDTH - SHARE_PROGESS_RIGHT - SHARE_PROGRESS_LEFT, SHARE_TOP_MARGIN); + + // Draw the text over the progress bar + uint16_t w = textWidth(&paintShare->toolbarFont, text); + uint16_t y = (SHARE_TOP_MARGIN - paintShare->toolbarFont.height) / 2; + drawText(&paintShare->toolbarFont, c000, text, (TFT_WIDTH - w) / 2, y); + if (arrows) + { + // flip instead of using rotation to prevent 1px offset + drawWsg(&paintShare->arrowWsg, (TFT_WIDTH - w) / 2 - paintShare->arrowWsg.w - 6, + y + (paintShare->toolbarFont.height - paintShare->arrowWsg.h) / 2, false, true, 90); + drawWsg(&paintShare->arrowWsg, (TFT_WIDTH - w) / 2 + w + 6, + y + (paintShare->toolbarFont.height - paintShare->arrowWsg.h) / 2, false, false, 90); + } + + if (bottomText != NULL) + { + if (paintShare->canvas.h > 0 && paintShare->canvas.yScale > 0) + { + y = paintShare->canvas.y + paintShare->canvas.h * paintShare->canvas.yScale + + (TFT_HEIGHT - paintShare->canvas.y - paintShare->canvas.h * paintShare->canvas.yScale + - paintShare->toolbarFont.height) + / 2; + } + else + { + y = TFT_HEIGHT - paintShare->toolbarFont.height - 8; + } + + w = textWidth(&paintShare->toolbarFont, bottomText); + drawText(&paintShare->toolbarFont, c000, bottomText, (TFT_WIDTH - w) / 2, y); + } +} + +void paintShareSendCanvas(void) +{ + PAINT_LOGI("Sending canvas metadata..."); + // Set the length to the canvas data packet length, plus one for the packet type + paintShare->sharePacketLen = PACKET_LEN_CANVAS_DATA + 1; + paintShare->sharePacket[0] = SHARE_PACKET_CANVAS_DATA; + + for (uint8_t i = 0; i < PAINT_MAX_COLORS; i++) + { + paintShare->sharePacket[i + 1] = paintShare->canvas.palette[i]; + } + + // pack the canvas dimensions in big-endian + // Height MSB + paintShare->sharePacket[PAINT_MAX_COLORS + 1] = ((uint8_t)((paintShare->canvas.h >> 8) & 0xFF)); + // Height LSB + paintShare->sharePacket[PAINT_MAX_COLORS + 2] = ((uint8_t)((paintShare->canvas.h >> 0) & 0xFF)); + // Width MSB + paintShare->sharePacket[PAINT_MAX_COLORS + 3] = ((uint8_t)((paintShare->canvas.w >> 8) & 0xFF)); + // Height LSB + paintShare->sharePacket[PAINT_MAX_COLORS + 4] = ((uint8_t)((paintShare->canvas.w >> 0) & 0xFF)); + + paintShare->shareState = SHARE_SEND_WAIT_CANVAS_DATA_ACK; + paintShare->shareNewPacket = false; + + p2pSendMsg(&paintShare->p2pInfo, paintShare->sharePacket, paintShare->sharePacketLen, paintShareP2pSendCb); +} + +void paintShareHandleCanvas(void) +{ + PAINT_LOGD("Handling %d bytes of canvas data", paintShare->sharePacketLen); + paintShare->shareNewPacket = false; + + if (paintShare->sharePacket[0] != SHARE_PACKET_CANVAS_DATA) + { + PAINT_LOGE("Canvas data has wrong type %d!!!", paintShare->sharePacket[0]); + return; + } + + for (uint8_t i = 0; i < PAINT_MAX_COLORS; i++) + { + paintShare->canvas.palette[i] = paintShare->sharePacket[i + 1]; + PAINT_LOGD("paletteMap[%d] = %d", paintShare->canvas.palette[i], i); + } + + paintShare->canvas.h + = (paintShare->sharePacket[PAINT_MAX_COLORS + 1] << 8) | (paintShare->sharePacket[PAINT_MAX_COLORS + 2]); + paintShare->canvas.w + = (paintShare->sharePacket[PAINT_MAX_COLORS + 3] << 8) | (paintShare->sharePacket[PAINT_MAX_COLORS + 4]); + + PAINT_LOGD("Canvas dimensions: %d x %d", paintShare->canvas.w, paintShare->canvas.h); + + uint8_t scale = paintGetMaxScale(paintShare->canvas.w, paintShare->canvas.h, SHARE_LEFT_MARGIN + SHARE_RIGHT_MARGIN, + SHARE_TOP_MARGIN + SHARE_BOTTOM_MARGIN); + paintShare->canvas.xScale = scale; + paintShare->canvas.yScale = scale; + + paintShare->canvas.x + = SHARE_LEFT_MARGIN + + (TFT_WIDTH - SHARE_LEFT_MARGIN - SHARE_RIGHT_MARGIN - paintShare->canvas.w * paintShare->canvas.xScale) / 2; + paintShare->canvas.y + = SHARE_TOP_MARGIN + + (TFT_HEIGHT - SHARE_TOP_MARGIN - SHARE_BOTTOM_MARGIN - paintShare->canvas.h * paintShare->canvas.yScale) + / 2; + + clearPxTft(); + drawRectFilledScaled(0, 0, paintShare->canvas.w, paintShare->canvas.h, c555, paintShare->canvas.x, + paintShare->canvas.y, paintShare->canvas.xScale, paintShare->canvas.yScale); + + paintShare->shareState = SHARE_RECV_PIXEL_DATA; + paintShareSendPixelRequest(); +} + +void paintShareSendPixels(void) +{ + paintShare->sharePacketLen = PAINT_SHARE_PX_PACKET_LEN + 3; + // Packet type header + paintShare->sharePacket[0] = SHARE_PACKET_PIXEL_DATA; + + // Packet seqnum + paintShare->sharePacket[1] = (uint8_t)((paintShare->shareSeqNum >> 8) & 0xFF); + paintShare->sharePacket[2] = (uint8_t)((paintShare->shareSeqNum >> 0) & 0xFF); + + for (uint8_t i = 0; i < PAINT_SHARE_PX_PACKET_LEN; i++) + { + if (PAINT_SHARE_PX_PER_PACKET * paintShare->shareSeqNum + i * 2 + >= (paintShare->canvas.w * paintShare->canvas.h)) + { + PAINT_LOGD("Breaking on last packet because %d * %d + %d * 2 >= %d * %d ---> %d >= %d", + PAINT_SHARE_PX_PER_PACKET, paintShare->shareSeqNum, i, paintShare->canvas.w, + paintShare->canvas.h, PAINT_SHARE_PX_PER_PACKET * paintShare->shareSeqNum + i * 2, + paintShare->canvas.w * paintShare->canvas.h); + paintShare->sharePacketLen = i + 3; + break; + } + // TODO dedupe this and the nvs functions into a paintSerialize() or something + uint16_t x0 = paintShare->canvas.x + + ((PAINT_SHARE_PX_PER_PACKET * paintShare->shareSeqNum) + (i * 2)) % paintShare->canvas.w + * paintShare->canvas.xScale; + uint16_t y0 = paintShare->canvas.y + + ((PAINT_SHARE_PX_PER_PACKET * paintShare->shareSeqNum) + (i * 2)) / paintShare->canvas.w + * paintShare->canvas.yScale; + uint16_t x1 = paintShare->canvas.x + + ((PAINT_SHARE_PX_PER_PACKET * paintShare->shareSeqNum) + (i * 2 + 1)) % paintShare->canvas.w + * paintShare->canvas.xScale; + uint16_t y1 = paintShare->canvas.y + + ((PAINT_SHARE_PX_PER_PACKET * paintShare->shareSeqNum) + (i * 2 + 1)) / paintShare->canvas.w + * paintShare->canvas.yScale; + + // PAINT_LOGD("Mapping px(%d, %d) (%d) --> %x", x0, y0, paintShare->disp->getPx(x0, y0), + // paintShare->sharePaletteMap[(uint8_t)(paintShare->disp->getPx(x0, y0))]); + paintShare->sharePacket[i + 3] = paintShare->sharePaletteMap[(uint8_t)getPxTft(x0, y0)] << 4 + | paintShare->sharePaletteMap[(uint8_t)getPxTft(x1, y1)]; + } + + paintShare->shareState = SHARE_SEND_WAIT_PIXEL_DATA_ACK; + PAINT_LOGD("p2pSendMsg(%p, %d)", paintShare->sharePacket, paintShare->sharePacketLen); + PAINT_LOGD("SENDING DATA:"); + for (uint8_t i = 0; i < paintShare->sharePacketLen; i += 4) + { + PAINT_LOGD("%04d %02x %02x %02x %02x", i, paintShare->sharePacket[i], paintShare->sharePacket[i + 1], + paintShare->sharePacket[i + 2], paintShare->sharePacket[i + 3]); + } + + p2pSendMsg(&paintShare->p2pInfo, paintShare->sharePacket, paintShare->sharePacketLen, paintShareP2pSendCb); +} + +void paintShareHandlePixels(void) +{ + PAINT_LOGD("Handling %d bytes of pixel data", paintShare->sharePacketLen); + paintShare->shareNewPacket = false; + + if (paintShare->sharePacket[0] != ((uint8_t)SHARE_PACKET_PIXEL_DATA)) + { + PAINT_LOGE("Received pixel data with incorrect type %d", paintShare->sharePacket[0]); + PAINT_LOGE("First 16 bytes: %02x%02x%02x%02x %02x%02x%02x%02x %02x%02x%02x%02x %02x%02x%02x%02x", + paintShare->sharePacket[0], paintShare->sharePacket[1], paintShare->sharePacket[2], + paintShare->sharePacket[3], paintShare->sharePacket[4], paintShare->sharePacket[5], + paintShare->sharePacket[6], paintShare->sharePacket[7], paintShare->sharePacket[8], + paintShare->sharePacket[9], paintShare->sharePacket[10], paintShare->sharePacket[11], + paintShare->sharePacket[12], paintShare->sharePacket[13], paintShare->sharePacket[14], + paintShare->sharePacket[15]); + return; + } + + paintShare->shareSeqNum = (paintShare->sharePacket[1] << 8) | paintShare->sharePacket[2]; + + PAINT_LOGD("Packet seqnum is %d (%x << 8 | %x)", paintShare->shareSeqNum, paintShare->sharePacket[1], + paintShare->sharePacket[2]); + + for (uint8_t i = 0; i < paintShare->sharePacketLen - 3; i++) + { + uint16_t x0 = ((PAINT_SHARE_PX_PER_PACKET * paintShare->shareSeqNum) + (i * 2)) % paintShare->canvas.w; + uint16_t y0 = ((PAINT_SHARE_PX_PER_PACKET * paintShare->shareSeqNum) + (i * 2)) / paintShare->canvas.w; + uint16_t x1 = ((PAINT_SHARE_PX_PER_PACKET * paintShare->shareSeqNum) + (i * 2 + 1)) % paintShare->canvas.w; + uint16_t y1 = ((PAINT_SHARE_PX_PER_PACKET * paintShare->shareSeqNum) + (i * 2 + 1)) / paintShare->canvas.w; + + setPxScaled(x0, y0, paintShare->canvas.palette[paintShare->sharePacket[i + 3] >> 4], paintShare->canvas.x, + paintShare->canvas.y, paintShare->canvas.xScale, paintShare->canvas.yScale); + setPxScaled(x1, y1, paintShare->canvas.palette[paintShare->sharePacket[i + 3] & 0xF], paintShare->canvas.x, + paintShare->canvas.y, paintShare->canvas.xScale, paintShare->canvas.yScale); + } + + PAINT_LOGD("We've received %d / %d pixels", + paintShare->shareSeqNum * PAINT_SHARE_PX_PER_PACKET + (paintShare->sharePacketLen - 3) * 2, + paintShare->canvas.h * paintShare->canvas.w); + + if (paintShare->shareSeqNum * PAINT_SHARE_PX_PER_PACKET + (paintShare->sharePacketLen - 3) * 2 + >= paintShare->canvas.h * paintShare->canvas.w) + { + PAINT_LOGD("I think we're done receiving"); + // We don't reeeally care if the sender acks this packet. + // I mean, it would be polite to make sure it gets there, but there's not really a point + paintShare->shareState = SHARE_RECV_SELECT_SLOT; + paintShareSendReceiveComplete(); + } + else + { + PAINT_LOGD("Done handling pixel packet, may we please have some more?"); + paintShareSendPixelRequest(); + } +} + +void paintShareSendPixelRequest(void) +{ + paintShare->sharePacket[0] = SHARE_PACKET_PIXEL_REQUEST; + paintShare->sharePacketLen = 1; + p2pSendMsg(&paintShare->p2pInfo, paintShare->sharePacket, paintShare->sharePacketLen, paintShareP2pSendCb); + paintShare->shareUpdateScreen = true; +} + +void paintShareSendReceiveComplete(void) +{ + paintShare->sharePacket[0] = SHARE_PACKET_RECEIVE_COMPLETE; + paintShare->sharePacketLen = 1; + p2pSendMsg(&paintShare->p2pInfo, paintShare->sharePacket, paintShare->sharePacketLen, paintShareP2pSendCb); + paintShare->shareUpdateScreen = true; +} + +// void paintShareSendAbort(void) +// { +// paintShare->sharePacket[0] = SHARE_PACKET_ABORT; +// paintShare->sharePacketLen = 1; +// p2pSendMsg(&paintShare->p2pInfo, paintShare->sharePacket, paintShare->sharePacketLen, paintShareP2pSendCb); +// paintShare->shareUpdateScreen = true; +// } + +void paintShareMsgSendOk(void) +{ + paintShare->shareUpdateScreen = true; + switch (paintShare->shareState) + { + case SHARE_SEND_SELECT_SLOT: + case SHARE_SEND_WAIT_FOR_CONN: + case SHARE_SEND_CANVAS_DATA: + break; + + case SHARE_SEND_WAIT_CANVAS_DATA_ACK: + { + PAINT_LOGD("Got ACK for canvas data!"); + paintShare->shareState = SHARE_SEND_WAIT_FOR_PIXEL_REQUEST; + break; + } + + case SHARE_SEND_WAIT_FOR_PIXEL_REQUEST: + break; + + case SHARE_SEND_PIXEL_DATA: + break; + + case SHARE_SEND_WAIT_PIXEL_DATA_ACK: + { + PAINT_LOGD("Got ACK for pixel data packet %d", paintShare->shareSeqNum); + + if (PAINT_SHARE_PX_PER_PACKET * (paintShare->shareSeqNum + 1) + >= (paintShare->canvas.w * paintShare->canvas.h)) + { + PAINT_LOGD("Probably done sending! But waiting for confirmation..."); + } + paintShare->shareState = SHARE_SEND_WAIT_FOR_PIXEL_REQUEST; + paintShare->shareSeqNum++; + break; + } + + case SHARE_SEND_COMPLETE: + { + break; + } + + case SHARE_RECV_WAIT_FOR_CONN: + { + break; + } + + case SHARE_RECV_WAIT_CANVAS_DATA: + { + break; + } + + case SHARE_RECV_PIXEL_DATA: + { + break; + } + + case SHARE_RECV_SELECT_SLOT: + { + break; + } + } +} + +void paintShareMsgSendFail(void) +{ + paintShareRetry(); +} + +void paintBeginShare(void) +{ + paintShareInitP2p(); + paintShare->shareState = SHARE_SEND_WAIT_FOR_CONN; + + paintShare->shareSeqNum = 0; + + PAINT_LOGD("Sender: Waiting for connection..."); +} + +void paintShareExitMode(void) +{ + p2pDeinit(&paintShare->p2pInfo); + freeFont(&paintShare->toolbarFont); + freeWsg(&paintShare->arrowWsg); + + free(paintShare); + + paintShare = NULL; +} + +void paintShareCheckForTimeout(void) +{ + if (paintShare->timeSincePacket >= CONN_LOST_TIMEOUT) + { + paintShare->timeSincePacket = 0; + PAINT_LOGD("Conn loss detected, resetting"); + paintShare->shareState = isSender() ? SHARE_SEND_WAIT_FOR_CONN : SHARE_RECV_WAIT_FOR_CONN; + paintShareInitP2p(); + } +} + +// Go back to the previous state so we retry the last thing +void paintShareRetry(void) +{ + PAINT_LOGE("Retrying something!"); + // is that all? + switch (paintShare->shareState) + { + case SHARE_SEND_SELECT_SLOT: + case SHARE_SEND_WAIT_FOR_CONN: + break; + + case SHARE_SEND_CANVAS_DATA: + { + paintShare->shareNewPacket = true; + break; + } + + case SHARE_SEND_WAIT_FOR_PIXEL_REQUEST: + break; + + case SHARE_SEND_WAIT_CANVAS_DATA_ACK: + { + paintShare->shareState = SHARE_SEND_CANVAS_DATA; + paintShare->shareNewPacket = true; + break; + } + + case SHARE_SEND_PIXEL_DATA: + { + paintShare->shareNewPacket = true; + break; + } + + case SHARE_SEND_WAIT_PIXEL_DATA_ACK: + { + paintShare->shareState = SHARE_SEND_PIXEL_DATA; + paintShare->shareNewPacket = true; + break; + } + + case SHARE_SEND_COMPLETE: + break; + + case SHARE_RECV_WAIT_FOR_CONN: + case SHARE_RECV_WAIT_CANVAS_DATA: + case SHARE_RECV_PIXEL_DATA: + case SHARE_RECV_SELECT_SLOT: + break; + } + paintShare->shareNewPacket = true; +} + +void paintShareMainLoop(int64_t elapsedUs) +{ + buttonEvt_t evt = {0}; + while (checkButtonQueueWrapper(&evt)) + { + paintShareButtonCb(&evt); + } + // Handle the sending of the packets and the other things + if (paintShare->clearScreen) + { + PAINT_LOGD("Redrawing!!!"); + paintShareDoLoad(); + paintShare->clearScreen = false; + } + + paintShare->shareTime += elapsedUs; + if (paintShare->shareNewPacket) + { + paintShare->timeSincePacket = 0; + } + else if (paintShare->shareState != SHARE_SEND_SELECT_SLOT && paintShare->shareState != SHARE_RECV_SELECT_SLOT + && paintShare->shareState != SHARE_SEND_WAIT_FOR_CONN + && paintShare->shareState != SHARE_RECV_WAIT_FOR_CONN) + { + paintShare->timeSincePacket += elapsedUs; + } + + switch (paintShare->shareState) + { + case SHARE_SEND_SELECT_SLOT: + break; + + case SHARE_SEND_WAIT_FOR_CONN: + paintShare->shareUpdateScreen = true; + if (!paintShare->connectionStarted) + { + paintSetRecentSlot(&paintShare->index, paintShare->shareSaveSlot); + paintShareInitP2p(); + } + break; + + case SHARE_SEND_CANVAS_DATA: + { + if (paintShare->shareNewPacket) + { + paintShareSendCanvas(); + } + break; + } + + case SHARE_SEND_WAIT_CANVAS_DATA_ACK: + { + paintShareCheckForTimeout(); + break; + } + + case SHARE_SEND_WAIT_FOR_PIXEL_REQUEST: + { + if (paintShare->shareNewPacket) + { + if (paintShare->sharePacket[0] == SHARE_PACKET_PIXEL_REQUEST) + { + paintShare->shareNewPacket = false; + paintShare->shareState = SHARE_SEND_PIXEL_DATA; + } + else if (paintShare->sharePacket[0] == SHARE_PACKET_RECEIVE_COMPLETE) + { + PAINT_LOGD("We've received confirmation! All data was received successfully"); + paintShare->shareNewPacket = false; + paintShare->shareState = SHARE_SEND_COMPLETE; + paintShare->shareUpdateScreen = true; + } + } + else + { + paintShareCheckForTimeout(); + } + break; + } + + case SHARE_SEND_PIXEL_DATA: + { + paintShareSendPixels(); + break; + } + + case SHARE_SEND_WAIT_PIXEL_DATA_ACK: + { + // Don't need to check for timeout! p2pConnection will handle it for acks + break; + } + + case SHARE_SEND_COMPLETE: + { + paintShareDeinitP2p(); + break; + } + + case SHARE_RECV_WAIT_FOR_CONN: + { + paintShare->shareUpdateScreen = true; + if (!paintShare->connectionStarted) + { + PAINT_LOGD("Reiniting p2p..."); + paintShareInitP2p(); + } + break; + } + + case SHARE_RECV_WAIT_CANVAS_DATA: + { + if (paintShare->shareNewPacket) + { + paintShareHandleCanvas(); + } + else + { + paintShareCheckForTimeout(); + } + paintShare->shareUpdateScreen = true; + + break; + } + + case SHARE_RECV_PIXEL_DATA: + { + if (paintShare->shareNewPacket) + { + paintShareHandlePixels(); + } + else + { + paintShareCheckForTimeout(); + } + paintShare->shareUpdateScreen = true; + break; + } + + case SHARE_RECV_SELECT_SLOT: + { + paintShareDeinitP2p(); + break; + } + } + + if (paintShare->shareUpdateScreen) + { + paintRenderShareMode(paintShare->shareTime); + paintShare->shareUpdateScreen = false; + } +} + +void paintShareButtonCb(buttonEvt_t* evt) +{ + if (paintShare->shareState == SHARE_SEND_SELECT_SLOT) + { + if (evt->down) + { + switch (evt->button) + { + case PB_LEFT: + { + // Load previous image + paintShare->shareSaveSlot = paintGetPrevSlotInUse(paintShare->index, paintShare->shareSaveSlot); + paintShare->clearScreen = true; + paintShare->shareUpdateScreen = true; + break; + } + case PB_RIGHT: + { + // Load next image + paintShare->shareSaveSlot = paintGetNextSlotInUse(paintShare->index, paintShare->shareSaveSlot); + paintShare->clearScreen = true; + paintShare->shareUpdateScreen = true; + break; + } + + case PB_A: + { + // Begin sharing! + paintBeginShare(); + paintShare->shareUpdateScreen = true; + break; + } + + case PB_B: + { + switchToSwadgeMode(&modePaint); + break; + } + + case PB_UP: + case PB_DOWN: + // Do Nothing! + case PB_SELECT: + case PB_START: + // Or do something on button up to avoid conflict with exit mode + break; + } + } + else + { + if (evt->button == PB_SELECT) + { + paintShare->shareSaveSlot = paintGetNextSlotInUse(paintShare->index, paintShare->shareSaveSlot); + paintShare->clearScreen = true; + paintShare->shareUpdateScreen = true; + } + else if (evt->button == PB_START) + { + paintBeginShare(); + paintShare->shareUpdateScreen = true; + } + } + } + else if (paintShare->shareState == SHARE_RECV_SELECT_SLOT) + { + if (evt->down) + { + switch (evt->button) + { + case PB_LEFT: + { + paintShare->shareSaveSlot = PREV_WRAP(paintShare->shareSaveSlot, PAINT_SAVE_SLOTS); + paintShare->shareUpdateScreen = true; + break; + } + + case PB_RIGHT: + { + paintShare->shareSaveSlot = NEXT_WRAP(paintShare->shareSaveSlot, PAINT_SAVE_SLOTS); + paintShare->shareUpdateScreen = true; + break; + } + + case PB_A: + { + paintShareDoSave(); + switchToSwadgeMode(&modePaint); + break; + } + + case PB_B: + { + // Exit without saving + switchToSwadgeMode(&modePaint); + break; + } + + case PB_UP: + case PB_DOWN: + // Do Nothing! + case PB_SELECT: + case PB_START: + // Or do something on button-up instead, to avoid overlap with SELECT+START + break; + } + } + else + { + if (evt->button == PB_START) + { + paintShareDoSave(); + switchToSwadgeMode(&modePaint); + } + else if (evt->button == PB_SELECT) + { + paintShare->shareSaveSlot = NEXT_WRAP(paintShare->shareSaveSlot, PAINT_SAVE_SLOTS); + paintShare->shareUpdateScreen = true; + } + } + // Does the receiver get any buttons? + // Yes! They need to pick their destination slot before starting P2P + } + else if (paintShare->shareState == SHARE_SEND_COMPLETE) + { + if (evt->down && evt->button == PB_B) + { + switchToSwadgeMode(&modePaint); + } + else + { + paintShare->shareState = SHARE_SEND_SELECT_SLOT; + paintShare->shareUpdateScreen = true; + } + } + else if (paintShare->shareState == SHARE_SEND_WAIT_FOR_CONN || paintShare->shareState == SHARE_RECV_WAIT_FOR_CONN) + { + if (evt->down && evt->button == PB_B) + { + switchToSwadgeMode(&modePaint); + } + } +} + +void paintShareRecvCb(const esp_now_recv_info_t* esp_now_info, const uint8_t* data, uint8_t len, int8_t rssi) +{ + p2pRecvCb(&paintShare->p2pInfo, esp_now_info->src_addr, (const uint8_t*)data, len, rssi); +} + +void paintShareP2pSendCb(p2pInfo* p2p, messageStatus_t status, const uint8_t* data, uint8_t len) +{ + switch (status) + { + case MSG_ACKED: + PAINT_LOGD("ACK"); + paintShareMsgSendOk(); + break; + + case MSG_FAILED: + PAINT_LOGE("FAILED!!!"); + paintShareMsgSendFail(); + break; + } +} + +void paintShareP2pConnCb(p2pInfo* p2p, connectionEvt_t evt) +{ + switch (evt) + { + case CON_STARTED: + PAINT_LOGD("CON_STARTED"); + break; + + case RX_GAME_START_ACK: + PAINT_LOGD("RX_GAME_START_ACK"); + break; + + case RX_GAME_START_MSG: + PAINT_LOGD("RX_GAME_START_MSG"); + break; + + case CON_ESTABLISHED: + { + PAINT_LOGD("CON_ESTABLISHED"); + if (paintShare->shareState == SHARE_SEND_WAIT_FOR_CONN) + { + PAINT_LOGD("state = SHARE_SEND_CANVAS_DATA"); + paintShare->shareState = SHARE_SEND_CANVAS_DATA; + paintShare->shareNewPacket = true; + } + else if (paintShare->shareState == SHARE_RECV_WAIT_FOR_CONN) + { + PAINT_LOGD("state = SHARE_RECV_WAIT_CANVAS_DATA"); + paintShare->shareState = SHARE_RECV_WAIT_CANVAS_DATA; + } + + break; + } + + case CON_LOST: + { + PAINT_LOGD("CON_LOST"); + paintShareInitP2p(); + + // We don't want to time out while waiting for a connection + paintShare->timeSincePacket = 0; + if (isSender()) + { + paintShare->shareState = SHARE_SEND_WAIT_FOR_CONN; + } + else + { + paintShare->shareState = SHARE_RECV_WAIT_FOR_CONN; + } + + break; + } + } +} + +void paintShareP2pMsgRecvCb(p2pInfo* p2p, const uint8_t* payload, uint8_t len) +{ + // no buffer overruns for me thanks + PAINT_LOGV("Receiving %d bytes via P2P callback", len); + memcpy(paintShare->sharePacket, payload, len); + + paintShare->sharePacketLen = len; + paintShare->shareNewPacket = true; +} + +void paintShareDoLoad(void) +{ + clearPxTft(); + // Load just image dimensions; + + if (!paintLoadDimensions(&paintShare->canvas, paintShare->shareSaveSlot)) + { + PAINT_LOGE("Failed to load dimensions, stopping load"); + return; + } + + // With the image dimensions, calculate the max scale that will fit on the screen + uint8_t scale = paintGetMaxScale(paintShare->canvas.w, paintShare->canvas.h, SHARE_LEFT_MARGIN + SHARE_RIGHT_MARGIN, + SHARE_TOP_MARGIN + SHARE_BOTTOM_MARGIN); + PAINT_LOGD("Loading image at scale %d", scale); + paintShare->canvas.xScale = scale; + paintShare->canvas.yScale = scale; + + // Center the canvas on the empty area of the screen + paintShare->canvas.x + = SHARE_LEFT_MARGIN + + (TFT_WIDTH - SHARE_LEFT_MARGIN - SHARE_RIGHT_MARGIN - paintShare->canvas.w * paintShare->canvas.xScale) / 2; + paintShare->canvas.y + = SHARE_TOP_MARGIN + + (TFT_HEIGHT - SHARE_TOP_MARGIN - SHARE_BOTTOM_MARGIN - paintShare->canvas.h * paintShare->canvas.yScale) + / 2; + + // Load the actual image! + // If all goes well, it will be drawn centered and as big as possible + paintLoad(&paintShare->index, &paintShare->canvas, paintShare->shareSaveSlot); + + for (uint8_t i = 0; i < PAINT_MAX_COLORS; i++) + { + paintShare->sharePaletteMap[(uint8_t)(paintShare->canvas.palette[i])] = i; + } +} + +void paintShareDoSave(void) +{ + paintSave(&paintShare->index, &paintShare->canvas, paintShare->shareSaveSlot); +} diff --git a/main/modes/mfpaint/paint_share.h b/main/modes/mfpaint/paint_share.h new file mode 100644 index 000000000..fe99b7c88 --- /dev/null +++ b/main/modes/mfpaint/paint_share.h @@ -0,0 +1,13 @@ +#ifndef _PAINT_SHARE_H_ +#define _PAINT_SHARE_H_ + +#include "swadge2024.h" + +#include "paint_common.h" + +extern swadgeMode_t modePaintShare; +extern swadgeMode_t modePaintReceive; + +extern paintShare_t* paintShare; + +#endif \ No newline at end of file diff --git a/main/modes/mfpaint/paint_song.c b/main/modes/mfpaint/paint_song.c new file mode 100644 index 000000000..2080763c4 --- /dev/null +++ b/main/modes/mfpaint/paint_song.c @@ -0,0 +1,165 @@ +#include "hdw-bzr.h" +#include "paint_song.h" + +static musicalNote_t paintBgmNotes[] = { + {.note = A_4, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, {.note = D_3, .timeMs = 62}, + {.note = SILENCE, .timeMs = 125}, {.note = F_SHARP_3, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, + {.note = D_3, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, {.note = C_SHARP_4, .timeMs = 62}, + {.note = SILENCE, .timeMs = 125}, {.note = C_SHARP_5, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, + {.note = D_3, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, {.note = A_3, .timeMs = 62}, + {.note = SILENCE, .timeMs = 125}, {.note = A_2, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, + {.note = C_SHARP_4, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, {.note = A_3, .timeMs = 62}, + {.note = SILENCE, .timeMs = 125}, {.note = F_SHARP_4, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, + {.note = F_SHARP_2, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, {.note = C_SHARP_5, .timeMs = 62}, + {.note = SILENCE, .timeMs = 125}, {.note = D_2, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, + {.note = A_2, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, {.note = A_4, .timeMs = 62}, + {.note = SILENCE, .timeMs = 125}, {.note = D_3, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, + {.note = F_SHARP_3, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, {.note = D_3, .timeMs = 62}, + {.note = SILENCE, .timeMs = 125}, {.note = C_SHARP_4, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, + {.note = C_SHARP_5, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, {.note = D_3, .timeMs = 62}, + {.note = SILENCE, .timeMs = 125}, {.note = A_3, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, + {.note = A_2, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, {.note = C_SHARP_4, .timeMs = 62}, + {.note = SILENCE, .timeMs = 125}, {.note = A_3, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, + {.note = F_SHARP_4, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, {.note = F_SHARP_2, .timeMs = 62}, + {.note = SILENCE, .timeMs = 125}, {.note = C_SHARP_5, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, + {.note = D_2, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, {.note = A_2, .timeMs = 62}, + {.note = SILENCE, .timeMs = 125}, {.note = F_SHARP_4, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, + {.note = B_2, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, {.note = D_3, .timeMs = 62}, + {.note = SILENCE, .timeMs = 125}, {.note = B_2, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, + {.note = A_3, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, {.note = A_4, .timeMs = 62}, + {.note = SILENCE, .timeMs = 125}, {.note = B_2, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, + {.note = F_SHARP_3, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, {.note = F_SHARP_2, .timeMs = 62}, + {.note = SILENCE, .timeMs = 125}, {.note = A_3, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, + {.note = F_SHARP_3, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, {.note = D_4, .timeMs = 62}, + {.note = SILENCE, .timeMs = 125}, {.note = D_2, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, + {.note = A_4, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, {.note = B_1, .timeMs = 62}, + {.note = SILENCE, .timeMs = 125}, {.note = F_SHARP_2, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, + {.note = F_SHARP_4, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, {.note = B_2, .timeMs = 62}, + {.note = SILENCE, .timeMs = 125}, {.note = D_3, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, + {.note = B_2, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, {.note = A_3, .timeMs = 62}, + {.note = SILENCE, .timeMs = 125}, {.note = A_4, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, + {.note = B_2, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, {.note = F_SHARP_3, .timeMs = 62}, + {.note = SILENCE, .timeMs = 125}, {.note = F_SHARP_2, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, + {.note = A_3, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, {.note = F_SHARP_3, .timeMs = 62}, + {.note = SILENCE, .timeMs = 125}, {.note = D_4, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, + {.note = D_2, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, {.note = A_4, .timeMs = 62}, + {.note = SILENCE, .timeMs = 125}, {.note = B_1, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, + {.note = F_SHARP_2, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, {.note = E_4, .timeMs = 62}, + {.note = SILENCE, .timeMs = 125}, {.note = A_2, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, + {.note = D_3, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, {.note = A_2, .timeMs = 62}, + {.note = SILENCE, .timeMs = 125}, {.note = G_3, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, + {.note = G_4, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, {.note = A_2, .timeMs = 62}, + {.note = SILENCE, .timeMs = 125}, {.note = E_3, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, + {.note = E_2, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, {.note = G_3, .timeMs = 62}, + {.note = SILENCE, .timeMs = 125}, {.note = E_3, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, + {.note = D_4, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, {.note = D_2, .timeMs = 62}, + {.note = SILENCE, .timeMs = 125}, {.note = G_4, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, + {.note = A_1, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, {.note = E_2, .timeMs = 62}, + {.note = SILENCE, .timeMs = 125}, {.note = E_4, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, + {.note = A_2, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, {.note = D_3, .timeMs = 62}, + {.note = SILENCE, .timeMs = 125}, {.note = A_2, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, + {.note = G_3, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, {.note = G_4, .timeMs = 62}, + {.note = SILENCE, .timeMs = 125}, {.note = A_2, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, + {.note = E_3, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, {.note = E_2, .timeMs = 62}, + {.note = SILENCE, .timeMs = 125}, {.note = G_3, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, + {.note = E_3, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, {.note = D_4, .timeMs = 62}, + {.note = SILENCE, .timeMs = 125}, {.note = D_2, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, + {.note = G_4, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, {.note = A_1, .timeMs = 62}, + {.note = SILENCE, .timeMs = 125}, {.note = E_2, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, + {.note = B_3, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, {.note = E_3, .timeMs = 62}, + {.note = SILENCE, .timeMs = 125}, {.note = A_1, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, + {.note = E_2, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, {.note = A_2, .timeMs = 62}, + {.note = SILENCE, .timeMs = 125}, {.note = C_SHARP_4, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, + {.note = A_3, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, {.note = E_3, .timeMs = 62}, + {.note = SILENCE, .timeMs = 125}, {.note = B_3, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, + {.note = D_4, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, {.note = E_3, .timeMs = 62}, + {.note = SILENCE, .timeMs = 125}, {.note = A_1, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, + {.note = G_3, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, {.note = E_4, .timeMs = 62}, + {.note = SILENCE, .timeMs = 125}, {.note = A_1, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, + {.note = E_3, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, {.note = C_SHARP_4, .timeMs = 62}, + {.note = SILENCE, .timeMs = 125}, {.note = A_3, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, + {.note = A_2, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, {.note = A_1, .timeMs = 62}, + {.note = SILENCE, .timeMs = 125}, {.note = A_1, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, + {.note = A_4, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, {.note = A_3, .timeMs = 62}, + {.note = SILENCE, .timeMs = 125}, {.note = E_3, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, + {.note = E_4, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, {.note = C_SHARP_4, .timeMs = 62}, + {.note = SILENCE, .timeMs = 125}, {.note = E_4, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, + {.note = F_SHARP_4, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, {.note = E_3, .timeMs = 62}, + {.note = SILENCE, .timeMs = 125}, {.note = E_4, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, + {.note = B_3, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, {.note = A_3, .timeMs = 62}, + {.note = SILENCE, .timeMs = 125}, {.note = C_SHARP_4, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, + {.note = F_SHARP_2, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, {.note = A_2, .timeMs = 62}, + {.note = SILENCE, .timeMs = 125}, {.note = F_SHARP_2, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, + {.note = E_3, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, {.note = A_3, .timeMs = 62}, + {.note = SILENCE, .timeMs = 125}, {.note = F_SHARP_2, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, + {.note = C_SHARP_3, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, {.note = C_SHARP_2, .timeMs = 62}, + {.note = SILENCE, .timeMs = 125}, {.note = E_3, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, + {.note = C_SHARP_3, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, {.note = A_3, .timeMs = 62}, + {.note = SILENCE, .timeMs = 125}, {.note = A_1, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, + {.note = E_4, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, {.note = F_SHARP_1, .timeMs = 62}, + {.note = SILENCE, .timeMs = 125}, {.note = C_SHARP_2, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, + {.note = C_SHARP_4, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, {.note = F_SHARP_2, .timeMs = 62}, + {.note = SILENCE, .timeMs = 125}, {.note = A_2, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, + {.note = F_SHARP_2, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, {.note = E_3, .timeMs = 62}, + {.note = SILENCE, .timeMs = 125}, {.note = A_3, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, + {.note = F_SHARP_2, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, {.note = C_SHARP_3, .timeMs = 62}, + {.note = SILENCE, .timeMs = 125}, {.note = C_SHARP_2, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, + {.note = E_3, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, {.note = C_SHARP_3, .timeMs = 62}, + {.note = SILENCE, .timeMs = 125}, {.note = A_3, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, + {.note = A_1, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, {.note = E_4, .timeMs = 62}, + {.note = SILENCE, .timeMs = 125}, {.note = F_SHARP_1, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, + {.note = C_SHARP_2, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, {.note = F_SHARP_4, .timeMs = 62}, + {.note = SILENCE, .timeMs = 125}, {.note = B_2, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, + {.note = D_3, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, {.note = B_2, .timeMs = 62}, + {.note = SILENCE, .timeMs = 125}, {.note = A_3, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, + {.note = A_4, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, {.note = B_2, .timeMs = 62}, + {.note = SILENCE, .timeMs = 125}, {.note = F_SHARP_3, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, + {.note = F_SHARP_2, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, {.note = A_3, .timeMs = 62}, + {.note = SILENCE, .timeMs = 125}, {.note = F_SHARP_3, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, + {.note = D_4, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, {.note = D_2, .timeMs = 62}, + {.note = SILENCE, .timeMs = 125}, {.note = A_4, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, + {.note = B_1, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, {.note = F_SHARP_2, .timeMs = 62}, + {.note = SILENCE, .timeMs = 125}, {.note = F_SHARP_4, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, + {.note = B_2, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, {.note = D_3, .timeMs = 62}, + {.note = SILENCE, .timeMs = 125}, {.note = B_2, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, + {.note = A_3, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, {.note = A_4, .timeMs = 62}, + {.note = SILENCE, .timeMs = 125}, {.note = B_2, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, + {.note = F_SHARP_3, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, {.note = F_SHARP_2, .timeMs = 62}, + {.note = SILENCE, .timeMs = 125}, {.note = A_3, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, + {.note = F_SHARP_3, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, {.note = D_4, .timeMs = 62}, + {.note = SILENCE, .timeMs = 125}, {.note = D_2, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, + {.note = A_4, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, {.note = B_1, .timeMs = 62}, + {.note = SILENCE, .timeMs = 125}, {.note = F_SHARP_2, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, + {.note = B_3, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, {.note = E_2, .timeMs = 62}, + {.note = SILENCE, .timeMs = 125}, {.note = G_2, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, + {.note = E_2, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, {.note = D_3, .timeMs = 62}, + {.note = SILENCE, .timeMs = 125}, {.note = D_4, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, + {.note = E_2, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, {.note = B_2, .timeMs = 62}, + {.note = SILENCE, .timeMs = 125}, {.note = B_1, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, + {.note = D_3, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, {.note = B_2, .timeMs = 62}, + {.note = SILENCE, .timeMs = 125}, {.note = G_3, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, + {.note = G_1, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, {.note = D_4, .timeMs = 62}, + {.note = SILENCE, .timeMs = 125}, {.note = E_1, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, + {.note = B_1, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, {.note = C_SHARP_4, .timeMs = 62}, + {.note = SILENCE, .timeMs = 125}, {.note = A_2, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, + {.note = A_2, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, {.note = A_2, .timeMs = 62}, + {.note = SILENCE, .timeMs = 125}, {.note = E_3, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, + {.note = E_4, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, {.note = A_2, .timeMs = 62}, + {.note = SILENCE, .timeMs = 125}, {.note = E_3, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, + {.note = C_SHARP_2, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, {.note = E_3, .timeMs = 62}, + {.note = SILENCE, .timeMs = 125}, {.note = E_3, .timeMs = 62}, {.note = SILENCE, .timeMs = 125}, + {.note = A_3, .timeMs = 62}, {.note = SILENCE, .timeMs = 500}, {.note = E_4, .timeMs = 62}, + {.note = SILENCE, .timeMs = 125}, +}; + +songTrack_t paintSongTrack = { + .numNotes = 442, + .loopStartNote = 0, + .notes = paintBgmNotes, +}; + +const song_t paintBgm = { + .numTracks = 1, + .shouldLoop = true, + .tracks = &paintSongTrack, +}; diff --git a/main/modes/mfpaint/paint_song.h b/main/modes/mfpaint/paint_song.h new file mode 100644 index 000000000..ebdf45f48 --- /dev/null +++ b/main/modes/mfpaint/paint_song.h @@ -0,0 +1,8 @@ +#ifndef _PAINT_SONG_H_ +#define _PAINT_SONG_H_ + +#include "hdw-bzr.h" + +extern const song_t paintBgm; + +#endif diff --git a/main/modes/mfpaint/paint_type.h b/main/modes/mfpaint/paint_type.h new file mode 100644 index 000000000..9e40ff0d9 --- /dev/null +++ b/main/modes/mfpaint/paint_type.h @@ -0,0 +1,133 @@ +#ifndef _PAINT_TYPE_H_ +#define _PAINT_TYPE_H_ + +#include + +#include "palette.h" + +// The number of colors in the palette and the max number of colors an image can be saved with +#define PAINT_MAX_COLORS 16 + +typedef paletteColor_t (*colorMapFn_t)(paletteColor_t col); + +/// @brief Defines each separate screen in the paint mode. +typedef enum +{ + // Top menu + PAINT_MENU, + // Control instructions + PAINT_HELP, + // Drawing mode + PAINT_DRAW, + // Select and view/edit saved drawings + PAINT_GALLERY, + // Share a drawing via ESPNOW + PAINT_SHARE, + // Receive a shared drawing over ESPNOW + PAINT_RECEIVE, +} paintScreen_t; + +/// @brief Represents the coordinates of a single pixel or point +typedef struct +{ + uint16_t x, y; +} point_t; + +typedef enum +{ + BTN_MODE_DRAW, + BTN_MODE_SELECT, + BTN_MODE_SAVE, + BTN_MODE_PALETTE, +} paintButtonMode_t; + +typedef enum +{ + PALETTE_R, + PALETTE_G, + PALETTE_B, +} paintEditPalette_t; + +typedef enum +{ + HIDDEN, + UNDO, + REDO, + PICK_SLOT_SAVE, + PICK_SLOT_LOAD, + CONFIRM_OVERWRITE, + CONFIRM_UNSAVED, + EDIT_PALETTE, + COLOR_PICKER, + CLEAR, + CONFIRM_CLEAR, + EXIT, + CONFIRM_EXIT, +} paintSaveMenu_t; + +// For tracking the state of the sharing / receiving process +typedef enum +{ + /////// Sender States + + // Sender is selecting slot to be shared (initial sender state) + SHARE_SEND_SELECT_SLOT, + + // Sender is waiting for connection + SHARE_SEND_WAIT_FOR_CONN, + + // Sender is sending canvas metadata + SHARE_SEND_CANVAS_DATA, + + // Sender sent canvas data and is waiting for ack from receiver + SHARE_SEND_WAIT_CANVAS_DATA_ACK, + + // Wait for the receiver to finish processing pixel data and request some more + SHARE_SEND_WAIT_FOR_PIXEL_REQUEST, + + // Sender got canvas data ack, is now sending pixel data packets + SHARE_SEND_PIXEL_DATA, + + // Sender sent pixel data, is waiting for ack + SHARE_SEND_WAIT_PIXEL_DATA_ACK, + + // All done! + SHARE_SEND_COMPLETE, + + //////// Receiver States + + // Receiver is waiting for connection (initial receiver state) + SHARE_RECV_WAIT_FOR_CONN, + + // Receiver is waiting for canvas metadata + SHARE_RECV_WAIT_CANVAS_DATA, + + // Receiver is receiving pixel data + SHARE_RECV_PIXEL_DATA, + + // Receiver has all pixel data and must pick save slot + SHARE_RECV_SELECT_SLOT, +} paintShareState_t; + +/// @brief Definition for a paintable screen region +typedef struct +{ + // The X and Y offset of the canvas's top-left pixel + uint16_t x, y; + + // The canvas's width and height, in "canvas pixels" + uint16_t w, h; + + // The X and Y scale of the canvas. Each "canvas pixel" will be drawn as [xScale x yScale] + uint8_t xScale, yScale; + + paletteColor_t palette[PAINT_MAX_COLORS]; +} paintCanvas_t; + +typedef struct +{ + paletteColor_t palette[PAINT_MAX_COLORS]; + uint8_t* px; +} paintUndo_t; + +#endif diff --git a/main/modes/mfpaint/paint_ui.c b/main/modes/mfpaint/paint_ui.c new file mode 100644 index 000000000..d065da982 --- /dev/null +++ b/main/modes/mfpaint/paint_ui.c @@ -0,0 +1,672 @@ +#include "paint_ui.h" + +#include + +#include "shapes.h" + +#include "paint_common.h" +#include "paint_util.h" +#include "paint_nvs.h" + +static const char startMenuUndo[] = "Undo"; +static const char startMenuRedo[] = "Redo"; +static const char startMenuSave[] = "Save"; +static const char startMenuLoad[] = "Load"; +static const char startMenuSlot[] = "Slot %d"; +static const char startMenuOverwrite[] = "Overwrite?"; +static const char startMenuYes[] = "Yes"; +static const char startMenuNo[] = "No"; +static const char startMenuClearCanvas[] = "New"; +static const char startMenuConfirmUnsaved[] = "Unsaved! OK?"; +static const char startMenuExit[] = "Exit"; +static const char startMenuEditPalette[] = "Edit Palette"; +static const char str_red[] = "Red"; +static const char str_green[] = "Green"; +static const char str_blue[] = "Blue"; + +void drawColorBox(uint16_t xOffset, uint16_t yOffset, uint16_t w, uint16_t h, paletteColor_t col, bool selected, + paletteColor_t topBorder, paletteColor_t bottomBorder) +{ + int dashLen = selected ? 1 : 0; + if (selected) + { + topBorder = c000; + bottomBorder = c000; + } + + if (col == cTransparent) + { + // Draw a lil checkerboard + fillDisplayArea(xOffset, yOffset, xOffset + w / 2, yOffset + h / 2, c111); + fillDisplayArea(xOffset + w / 2, yOffset, xOffset + w, yOffset + h / 2, c555); + fillDisplayArea(xOffset, yOffset + h / 2, xOffset + w / 2, yOffset + h, c555); + fillDisplayArea(xOffset + w / 2, yOffset + h / 2, xOffset + w, yOffset + h, c111); + } + else + { + fillDisplayArea(xOffset, yOffset, xOffset + w, yOffset + h, col); + } + + if (topBorder != cTransparent) + { + // Top border + drawLine(xOffset - 1, yOffset, xOffset + w - 1, yOffset, topBorder, dashLen); + // Left border + drawLine(xOffset - 1, yOffset, xOffset - 1, yOffset + h - 1, topBorder, dashLen); + } + + if (bottomBorder != cTransparent) + { + // Bottom border + drawLine(xOffset, yOffset + h, xOffset + w - 1, yOffset + h, bottomBorder, dashLen); + // Right border + drawLine(xOffset + w, yOffset + 1, xOffset + w, yOffset + h - 1, bottomBorder, dashLen); + } +} + +void paintRenderToolbar(paintArtist_t* artist, paintCanvas_t* canvas, paintDraw_t* paintState, + const brush_t* firstBrush, const brush_t* lastBrush) +{ + //////// Background + bool toolWheelVisible = wheelMenuActive(paintState->toolWheel, paintState->toolWheelRenderer); + + if (toolWheelVisible) + { + // Clear whole screen + fillDisplayArea(0, 0, TFT_WIDTH, TFT_HEIGHT, PAINT_TOOLBAR_BG); + + // Draw the palette + /*uint16_t colorBoxX = PAINT_COLORBOX_MARGIN_X + (paintState->canvas.x - 1 - PAINT_COLORBOX_W - + PAINT_COLORBOX_MARGIN_X * 2 - 2) / 2; uint16_t colorBoxY = PAINT_ACTIVE_COLOR_Y + PAINT_COLORBOX_W + + PAINT_COLORBOX_W / 2 + 1 + PAINT_COLORBOX_MARGIN_TOP; + + // vertically center the color boxes in the available space + colorBoxY = colorBoxY + (TFT_HEIGHT - PAINT_COLORBOX_MARGIN_TOP - (PAINT_MAX_COLORS * (PAINT_COLORBOX_MARGIN_TOP + + PAINT_COLORBOX_H)) - colorBoxY - PAINT_COLORBOX_MARGIN_TOP) / 2; + + //////// Recent Colors (palette) + for (int i = 0; i < PAINT_MAX_COLORS; i++) + { + drawColorBox(colorBoxX, colorBoxY + i * (PAINT_COLORBOX_MARGIN_TOP + PAINT_COLORBOX_H), PAINT_COLORBOX_W, + PAINT_COLORBOX_H, canvas->palette[i], false, PAINT_COLORBOX_SHADOW_TOP, PAINT_COLORBOX_SHADOW_BOTTOM); + } + + if (paintState->buttonMode == BTN_MODE_SELECT || paintState->buttonMode == BTN_MODE_PALETTE) + { + // Draw a slightly bigger color box for the selected color + drawColorBox(colorBoxX - 3, colorBoxY + paintState->paletteSelect * (PAINT_COLORBOX_MARGIN_TOP + + PAINT_COLORBOX_H) - 3, PAINT_COLORBOX_W + 6, PAINT_COLORBOX_H + 6, canvas->palette[paintState->paletteSelect], + true, PAINT_COLORBOX_SHADOW_TOP, PAINT_COLORBOX_SHADOW_BOTTOM); + }*/ + } + else + { + // Fill top bar + fillDisplayArea(0, 0, TFT_WIDTH, canvas->y, PAINT_TOOLBAR_BG); + + // Fill left side bar + fillDisplayArea(0, 0, canvas->x, TFT_HEIGHT, PAINT_TOOLBAR_BG); + + // Fill right bar, if there's room + if (canvas->x + canvas->w * canvas->xScale < TFT_WIDTH) + { + fillDisplayArea(canvas->x + canvas->w * canvas->xScale, 0, TFT_WIDTH, TFT_HEIGHT, PAINT_TOOLBAR_BG); + } + + // Fill bottom bar, if there's room + if (canvas->y + canvas->h * canvas->yScale < TFT_HEIGHT) + { + fillDisplayArea(0, canvas->y + canvas->h * canvas->yScale, TFT_WIDTH, TFT_HEIGHT, PAINT_TOOLBAR_BG); + } + + // Draw border around canvas + drawRect(canvas->x - 1, canvas->y - 1, canvas->x + canvas->w * canvas->xScale + 1, + canvas->y + canvas->h * canvas->yScale + 1, c000); + } + + //////// Active Colors + // Draw the background color, then draw the foreground color overlapping it and offset by half in both directions + drawColorBox(PAINT_ACTIVE_COLOR_X - 1, PAINT_ACTIVE_COLOR_Y, PAINT_COLORBOX_W, PAINT_COLORBOX_H, artist->bgColor, + false, PAINT_COLORBOX_SHADOW_TOP, PAINT_COLORBOX_SHADOW_BOTTOM); + drawColorBox(PAINT_ACTIVE_COLOR_X + PAINT_COLORBOX_W / 2, PAINT_ACTIVE_COLOR_Y + PAINT_COLORBOX_H / 2, + PAINT_COLORBOX_W, PAINT_COLORBOX_H, artist->fgColor, false, cTransparent, + PAINT_COLORBOX_SHADOW_BOTTOM); + + uint16_t textX = 30, textY = (paintState->canvas.y - 1 - 2 * PAINT_TOOLBAR_TEXT_PADDING_Y) / 2; + + // Up/down arrow logic, for all the options that have text at the top + if (paintState->saveMenu != HIDDEN && paintState->saveMenu != COLOR_PICKER) + { + // Up arrow 1px above center of text + drawWsg(&paintState->smallArrowWsg, textX, + textY + paintState->saveMenuFont.height / 2 - paintState->smallArrowWsg.h - 1, false, false, 0); + + // Down arrow 1px below center of text + drawWsg(&paintState->smallArrowWsg, textX, textY + paintState->saveMenuFont.height / 2 + 1, false, false, 180); + + // Move the text over to accomodate the arrow, plus 4px spacing + textX += paintState->smallArrowWsg.w + 4; + } + + bool drawYesNo = false; + + if (paintState->saveMenu == HIDDEN) + { + //////// Tools + + // Draw the brush icons + /*uint16_t iconOffset = 30; + for (const brush_t* curBrush = firstBrush; curBrush <= lastBrush; curBrush++) + { + const wsg_t* brushIcon = (curBrush == artist->brushDef) ? &curBrush->iconActive : &curBrush->iconInactive; + uint16_t iconY = (paintState->canvas.y - 1 - brushIcon->h) / 2; + + uint16_t maxIconBottom = 0; + if (iconY + brushIcon->h > maxIconBottom) + { + maxIconBottom = iconY + brushIcon->h; + } + + // if this is the active brush, draw the FG color underneath it first + if (curBrush == artist->brushDef) + { + fillDisplayArea(iconOffset, iconY, iconOffset + brushIcon->w, iconY + brushIcon->h, + (paintState->buttonMode == BTN_MODE_SELECT) ? canvas->palette[paintState->paletteSelect] : artist->fgColor); + } + + drawWsg(brushIcon, iconOffset, iconY, false, false, 0); + + iconOffset += brushIcon->w + 1; + }*/ + + // Draw the brush size, if applicable and not constant + char text[16]; + + textX = PAINT_ACTIVE_COLOR_X + PAINT_COLORBOX_W + PAINT_COLORBOX_W / 2 + PAINT_COLORBOX_MARGIN_X + 1; + textY = TFT_HEIGHT - paintState->toolbarFont.height - 4; + + fillDisplayArea(textX + 1, textY + paintState->toolbarFont.height - artist->brushDef->iconActive.h, + textX + 1 + artist->brushDef->iconActive.w, textY + paintState->toolbarFont.height, + (paintState->buttonMode == BTN_MODE_SELECT) ? canvas->palette[paintState->paletteSelect] + : artist->fgColor); + drawWsgSimple(&artist->brushDef->iconActive, textX + 1, + textY + paintState->toolbarFont.height - artist->brushDef->iconActive.h); + drawRect(textX, textY + paintState->toolbarFont.height - artist->brushDef->iconActive.h - 1, + textX + artist->brushDef->iconActive.w + 2, textY + paintState->toolbarFont.height + 1, c000); + + textX += artist->brushDef->iconActive.w + PAINT_COLORBOX_MARGIN_X + 2; + + // Draw the brush name + textX = drawText(&paintState->toolbarFont, c000, artist->brushDef->name, textX, textY); + + if (artist->brushDef->minSize != artist->brushDef->maxSize) + { + if (artist->brushWidth == 0) + { + snprintf(text, sizeof(text), "Auto"); + } + else + { + snprintf(text, sizeof(text), "%d", artist->brushWidth); + } + + textX += 4; + // Draw the icon on the text's baseline + drawWsg(&paintState->brushSizeWsg, textX, + textY + paintState->toolbarFont.height - paintState->brushSizeWsg.h, false, false, 0); + textX += paintState->brushSizeWsg.w + 1; + textX = drawText(&paintState->toolbarFont, c000, text, textX, textY); + } + + if (artist->brushDef->mode == PICK_POINT && artist->brushDef->maxPoints > 1) + { + // Draw the number of picks made / total + snprintf(text, sizeof(text), "%" PRIu32 "/%d", (uint32_t)pxStackSize(&artist->pickPoints), + artist->brushDef->maxPoints); + + textX += 4; + drawWsg(&paintState->picksWsg, textX, textY + paintState->toolbarFont.height - paintState->picksWsg.h, + false, false, 0); + textX += paintState->picksWsg.w + 1; + drawText(&paintState->toolbarFont, c000, text, textX, textY); + } + else if (artist->brushDef->mode == PICK_POINT_LOOP && artist->brushDef->maxPoints > 1) + { + // Draw the number of remaining picks + uint8_t maxPicks = artist->brushDef->maxPoints; + + if (pxStackSize(&artist->pickPoints) + 1 == maxPicks - 1) + { + snprintf(text, sizeof(text), "Last"); + } + else + { + snprintf(text, sizeof(text), "%" PRIu32, (uint32_t)(maxPicks - pxStackSize(&artist->pickPoints) - 1)); + } + + textX += 4; + drawWsg(&paintState->picksWsg, textX, textY + paintState->toolbarFont.height - paintState->picksWsg.h, + false, false, 0); + textX += paintState->picksWsg.w + 1; + drawText(&paintState->toolbarFont, c000, text, textX, textY); + } + } + else if (paintState->saveMenu == UNDO) + { + drawText(&paintState->saveMenuFont, c000, startMenuUndo, textX, textY); + } + else if (paintState->saveMenu == REDO) + { + drawText(&paintState->saveMenuFont, c000, startMenuRedo, textX, textY); + } + else if (paintState->saveMenu == PICK_SLOT_SAVE || paintState->saveMenu == PICK_SLOT_LOAD) + { + bool saving = paintState->saveMenu == PICK_SLOT_SAVE; + // Draw "Save" / "Load" + drawText(&paintState->saveMenuFont, c000, saving ? startMenuSave : startMenuLoad, textX, textY); + + const wsg_t* fileIcon = NULL; + + if (saving) + { + fileIcon = paintGetSlotInUse(paintState->index, paintState->selectedSlot) ? &paintState->overwriteWsg + : &paintState->newfileWsg; + } + + // Draw the slot number + char text[16]; + snprintf(text, sizeof(text), startMenuSlot, paintState->selectedSlot + 1); + uint16_t textW = textWidth(&paintState->saveMenuFont, text); + + // Text goes all the way to the right, minus 13px for the corner, then the arrow, arrow spacing, [icon and + // spacing,] and the text itself + textX = TFT_WIDTH - 13 - paintState->bigArrowWsg.w - 4 - (fileIcon ? fileIcon->w + 4 : 0) - textW; + drawText(&paintState->saveMenuFont, c000, text, textX, textY); + + if (fileIcon) + { + drawWsgSimple(fileIcon, textX + textW + 4, textY); + } + + // Left arrow + drawWsg(&paintState->bigArrowWsg, textX - 4 - paintState->bigArrowWsg.w, + textY + (paintState->saveMenuFont.height - paintState->bigArrowWsg.h) / 2, false, false, 270); + // Right arrow + drawWsg(&paintState->bigArrowWsg, textX + textW + 4 + (fileIcon ? fileIcon->w + 4 : 0), + textY + (paintState->saveMenuFont.height - paintState->bigArrowWsg.h) / 2, false, false, 90); + } + else if (paintState->saveMenu == CONFIRM_OVERWRITE) + { + // Draw "Overwrite?" + drawText(&paintState->saveMenuFont, c000, startMenuOverwrite, textX, textY); + + drawYesNo = true; + } + else if (paintState->saveMenu == CLEAR) + { + drawText(&paintState->saveMenuFont, c000, startMenuClearCanvas, textX, textY); + } + else if (paintState->saveMenu == CONFIRM_CLEAR || paintState->saveMenu == CONFIRM_EXIT + || paintState->saveMenu == CONFIRM_UNSAVED) + { + // Draw "Unsaved! OK?" + drawText(&paintState->saveMenuFont, c000, startMenuConfirmUnsaved, textX, textY); + + drawYesNo = true; + } + else if (paintState->saveMenu == EXIT) + { + drawText(&paintState->saveMenuFont, c000, startMenuExit, textX, textY); + } + else if (paintState->saveMenu == EDIT_PALETTE) + { + drawText(&paintState->saveMenuFont, c000, startMenuEditPalette, textX, textY); + } + else if (paintState->saveMenu == COLOR_PICKER) + { + paintRenderColorPicker(artist, canvas, paintState); + } + + if (drawYesNo) + { + // Draw "Yes" / "No" + const char* optionText = (paintState->saveMenuBoolOption ? startMenuYes : startMenuNo); + uint16_t textW = textWidth(&paintState->saveMenuFont, optionText); + textX = TFT_WIDTH - 13 - paintState->bigArrowWsg.w - 4 - textW; + + drawText(&paintState->saveMenuFont, c000, optionText, textX, textY); + + // Left arrow + drawWsg(&paintState->bigArrowWsg, textX - 4 - paintState->bigArrowWsg.w, + textY + (paintState->saveMenuFont.height - paintState->bigArrowWsg.h) / 2, false, false, 270); + // Right arrow + drawWsg(&paintState->bigArrowWsg, textX + textW + 4, + textY + (paintState->saveMenuFont.height - paintState->bigArrowWsg.h) / 2, false, false, 90); + } +} + +uint16_t paintRenderGradientBox(paintCanvas_t* canvas, char channel, paletteColor_t col, uint16_t x, uint16_t y, + uint16_t barW, uint16_t h, bool selected) +{ + uint16_t channelVal; + switch (channel) + { + case 'r': + channelVal = col / 36; + break; + case 'g': + channelVal = (col / 6) % 6; + break; + case 'b': + channelVal = col % 6; + break; + default: + channelVal = 0; + break; + } + + // draw the color bar... under the text box? + for (uint8_t i = 0; i < 6; i++) + { + uint16_t r = channel == 'r' ? i : (col / 36); + uint16_t g = channel == 'g' ? i : (col / 6) % 6; + uint16_t b = channel == 'b' ? i : (col % 6); + + fillDisplayArea(x + i * barW, y, x + i * barW + barW, y + h + 1, r * 36 + g * 6 + b); + } + + // Draw a bigger box for the active color in this segment + fillDisplayArea(x + channelVal * barW - 1, y - 1, x + channelVal * barW + barW + 1, y + h + 2, col); + + // Border around selected segment, ~if this channel is selected~ + if (selected) + { + // Inner border + drawRect(x + channelVal * barW - 2, y - 2, x + channelVal * barW + barW + 2, y + h + 3, c000); + + // Top + drawLine(x + channelVal * barW - 2, y - 3, x + channelVal * barW + barW + 1, y - 3, c555, 0); + // Left + drawLine(x + channelVal * barW - 3, y - 2, x + channelVal * barW - 3, y + h + 2, c555, 0); + // Right + drawLine(x + channelVal * barW + barW + 2, y - 2, x + channelVal * barW + barW + 2, y + h + 2, c555, 0); + // Bottom + drawLine(x + channelVal * barW - 2, y + h + 3, x + channelVal * barW + barW + 1, y + h + 3, c555, 0); + } + + // return total width of box + return 6 * barW + 2; +} + +void paintRenderColorPicker(paintArtist_t* artist, paintCanvas_t* canvas, paintDraw_t* paintState) +{ + bool rCur = false, bCur = false, gCur = false; + + if (paintState->editPaletteCur == &paintState->editPaletteR) + { + // R selected + rCur = true; + } + else if (paintState->editPaletteCur == &paintState->editPaletteG) + { + // G selected + gCur = true; + } + else + { + // B selected + bCur = true; + } + + // Draw 3 color gradient bars, each showing what the color would be if it were changed + uint16_t barOffset = canvas->x, barMargin = 4; + uint16_t barY = paintState->smallFont.height + 2 + 2; + uint16_t barH = canvas->y - barY - 2 - 2 - 1; + + uint16_t textW = textWidth(&paintState->smallFont, str_red); + uint16_t barWidth = paintRenderGradientBox(canvas, 'r', paintState->newColor, barOffset, barY, + PAINT_COLOR_PICKER_BAR_W, barH, rCur); + drawText(&paintState->smallFont, c000, str_red, barOffset + (barWidth - textW) / 2, 1); + barOffset += barWidth + barMargin; + + textW = textWidth(&paintState->smallFont, str_green); + barWidth = paintRenderGradientBox(canvas, 'g', paintState->newColor, barOffset, barY, PAINT_COLOR_PICKER_BAR_W, + barH, gCur); + drawText(&paintState->smallFont, c000, str_green, barOffset + (barWidth - textW) / 2, 1); + barOffset += barWidth + barMargin; + + textW = textWidth(&paintState->smallFont, str_blue); + barWidth = paintRenderGradientBox(canvas, 'b', paintState->newColor, barOffset, barY, PAINT_COLOR_PICKER_BAR_W, + barH, bCur); + drawText(&paintState->smallFont, c000, str_blue, barOffset + (barWidth - textW) / 2, 1); + barOffset += barWidth + barMargin; + + char hexCode[16]; + snprintf(hexCode, sizeof(hexCode), "#%02X%02X%02X", paintState->editPaletteR * 51, paintState->editPaletteG * 51, + paintState->editPaletteB * 51); + + textW = textWidth(&paintState->toolbarFont, hexCode); + + uint16_t hexW = canvas->x + canvas->w * canvas->xScale - barOffset; + // Make sure the color box is wide enough for the hex text + if (hexW < textW + 4) + { + hexW = textW + 4; + } + + // Draw a color box the same height as the gradient bars, extendng at least to the end of the canva + drawColorBox(barOffset, barY, hexW, barH, paintState->newColor, false, c000, c000); + + // Draw the hex code for the color centered (vertically + horizontally) in the box + drawText(&paintState->toolbarFont, getContrastingColorBW(paintState->newColor), hexCode, + barOffset + (hexW - textW) / 2, barY + (barH - paintState->toolbarFont.height) / 2); +} + +void paintClearCanvas(const paintCanvas_t* canvas, paletteColor_t bgColor) +{ + fillDisplayArea(canvas->x, canvas->y, canvas->x + canvas->w * canvas->xScale, + canvas->y + canvas->h * canvas->yScale, bgColor); +} + +// Generates a cursor sprite that's a box +bool paintGenerateCursorSprite(wsg_t* cursorWsg, const paintCanvas_t* canvas, uint8_t size) +{ + uint16_t newW = size * canvas->xScale + 2; + uint16_t newH = size * canvas->yScale + 2; + + void* newData = malloc(sizeof(paletteColor_t) * newW * newH); + if (newData == NULL) + { + // Don't continue if allocation failed + return false; + } + + cursorWsg->w = newW; + cursorWsg->h = newH; + cursorWsg->px = newData; + + paletteColor_t pxVal; + for (uint16_t x = 0; x < cursorWsg->w; x++) + { + for (uint16_t y = 0; y < cursorWsg->h; y++) + { + if (x == 0 || x == cursorWsg->w - 1 || y == 0 || y == cursorWsg->h - 1) + { + pxVal = c000; + } + else + { + pxVal = cTransparent; + } + cursorWsg->px[y * cursorWsg->w + x] = pxVal; + } + } + + return true; +} + +void paintFreeCursorSprite(wsg_t* cursorWsg) +{ + if (cursorWsg->px != NULL) + { + free(cursorWsg->px); + cursorWsg->px = NULL; + cursorWsg->w = 0; + cursorWsg->h = 0; + } +} + +void initCursor(paintCursor_t* cursor, paintCanvas_t* canvas, const wsg_t* sprite) +{ + cursor->sprite = sprite; + + cursor->show = false; + cursor->x = 0; + cursor->y = 0; + + cursor->redraw = true; + + initPxStack(&cursor->underPxs); +} + +void deinitCursor(paintCursor_t* cursor) +{ + freePxStack(&cursor->underPxs); +} + +void setCursorSprite(paintCursor_t* cursor, paintCanvas_t* canvas, const wsg_t* sprite) +{ + undrawCursor(cursor, canvas); + + cursor->sprite = sprite; + cursor->redraw = true; + + drawCursor(cursor, canvas); +} + +void setCursorOffset(paintCursor_t* cursor, int16_t x, int16_t y) +{ + cursor->spriteOffsetX = x; + cursor->spriteOffsetY = y; + cursor->redraw = true; +} + +/// @brief Undraws the cursor and removes its pixels from the stack +/// @param cursor The cursor to hide +/// @param canvas The canvas to hide the cursor pixels from +void undrawCursor(paintCursor_t* cursor, paintCanvas_t* canvas) +{ + while (popPx(&cursor->underPxs)) + ; + + cursor->redraw = true; +} + +/// @brief Hides the cursor without removing its stored pixels from the stack +/// @param cursor The cursor to hide +/// @param canvas The canvas to hide the cursor pixels from +void hideCursor(paintCursor_t* cursor, paintCanvas_t* canvas) +{ + if (cursor->show) + { + undrawCursor(cursor, canvas); + + cursor->show = false; + cursor->redraw = true; + } +} + +/// @brief Shows the cursor without saving the pixels under it +/// @param cursor The cursor to show +/// @param canvas The canvas to draw the cursor on +/// @return true if the cursor was shown, or false if it could not due to memory constraints +bool showCursor(paintCursor_t* cursor, paintCanvas_t* canvas) +{ + if (!cursor->show) + { + cursor->show = true; + cursor->redraw = true; + return drawCursor(cursor, canvas); + } + + return true; +} + +/// @brief If not hidden, draws the cursor on the canvas and saves the pixels for later. If hidden, does nothing. +/// @param cursor The cursor to draw +/// @param canvas The canvas to draw it on and save the pixels from +/// @return true if the cursor was drawn, or false if it could not be due to memory constraints +bool drawCursor(paintCursor_t* cursor, paintCanvas_t* canvas) +{ + bool cursorIsNearEdge = (canvasToDispX(canvas, cursor->x) + cursor->spriteOffsetX < canvas->x + || canvasToDispX(canvas, cursor->x) + cursor->spriteOffsetX + cursor->sprite->w + > canvas->x + canvas->w * canvas->xScale + || canvasToDispY(canvas, cursor->y) + cursor->spriteOffsetY < canvas->y + || canvasToDispY(canvas, cursor->y) + cursor->spriteOffsetY + cursor->sprite->h + > canvas->y + canvas->h * canvas->yScale); + if (cursor->show && (cursor->redraw || cursorIsNearEdge)) + { + // Undraw the previous cursor pixels, if there are any + undrawCursor(cursor, canvas); + if (!paintDrawWsgTemp(cursor->sprite, &cursor->underPxs, + canvasToDispX(canvas, cursor->x) + cursor->spriteOffsetX, + canvasToDispY(canvas, cursor->y) + cursor->spriteOffsetY, getContrastingColor)) + { + // Return false if we couldn't draw/save the cursor + return false; + } + cursor->redraw = false; + } + + return true; +} + +/// @brief Moves the cursor by the given relative x and y offsets, staying within the canvas bounds. Does not draw. +/// @param cursor The cursor to be moved +/// @param canvas The canvas for the cursor bounds +/// @param xDiff The relative X offset to move the cursor +/// @param yDiff The relative Y offset to move the cursor +void moveCursorRelative(paintCursor_t* cursor, paintCanvas_t* canvas, int16_t xDiff, int16_t yDiff) +{ + int16_t newX, newY; + + newX = cursor->x + xDiff; + newY = cursor->y + yDiff; + + if (newX >= canvas->w) + { + newX = canvas->w - 1; + } + else if (newX < 0) + { + newX = 0; + } + + if (newY >= canvas->h) + { + newY = canvas->h - 1; + } + else if (newY < 0) + { + newY = 0; + } + + // Only update the position if it would be different from the current position. + // TODO: Does this actually matter? + if (newX != cursor->x || newY != cursor->y) + { + cursor->redraw = true; + cursor->x = newX; + cursor->y = newY; + } +} + +void moveCursorAbsolute(paintCursor_t* cursor, paintCanvas_t* canvas, uint16_t x, uint16_t y) +{ + if (x < canvas->w && y < canvas->h) + { + cursor->redraw = true; + cursor->x = x; + cursor->y = y; + } +} diff --git a/main/modes/mfpaint/paint_ui.h b/main/modes/mfpaint/paint_ui.h new file mode 100644 index 000000000..ce2623c8d --- /dev/null +++ b/main/modes/mfpaint/paint_ui.h @@ -0,0 +1,42 @@ +#ifndef _PAINT_UI_H_ +#define _PAINT_UI_H_ + +#include +#include + +#include "palette.h" + +#include "paint_type.h" +#include "paint_common.h" + +void restoreCursorPixels(void); + +void plotCursor(void); +void paintRenderCursor(void); + +void drawColorBox(uint16_t xOffset, uint16_t yOffset, uint16_t w, uint16_t h, paletteColor_t col, bool selected, + paletteColor_t topBorder, paletteColor_t bottomBorder); +void paintRenderToolbar(paintArtist_t* artist, paintCanvas_t* canvas, paintDraw_t* paintState, + const brush_t* firstBrush, const brush_t* lastBrush); +uint16_t paintRenderGradientBox(paintCanvas_t* canvas, char channel, paletteColor_t col, uint16_t x, uint16_t y, + uint16_t barW, uint16_t h, bool selected); +void paintRenderColorPicker(paintArtist_t* artist, paintCanvas_t* canvas, paintDraw_t* paintState); +void paintRenderAll(void); + +void paintClearCanvas(const paintCanvas_t* canvas, paletteColor_t bgColor); + +bool paintGenerateCursorSprite(wsg_t* sprite, const paintCanvas_t* canvas, uint8_t size); +void paintFreeCursorSprite(wsg_t* sprite); + +void initCursor(paintCursor_t* cursor, paintCanvas_t* canvas, const wsg_t* sprite); +void deinitCursor(paintCursor_t* cursor); +void setCursorSprite(paintCursor_t* cursor, paintCanvas_t* canvas, const wsg_t* sprite); +void setCursorOffset(paintCursor_t* cursor, int16_t x, int16_t y); +void undrawCursor(paintCursor_t* cursor, paintCanvas_t* canvas); +void hideCursor(paintCursor_t* cursor, paintCanvas_t* canvas); +bool showCursor(paintCursor_t* cursor, paintCanvas_t* canvas); +bool drawCursor(paintCursor_t* cursor, paintCanvas_t* canvas); +void moveCursorRelative(paintCursor_t* cursor, paintCanvas_t* canvas, int16_t xDiff, int16_t yDiff); +void moveCursorAbsolute(paintCursor_t* cursor, paintCanvas_t* canvas, uint16_t x, uint16_t y); + +#endif diff --git a/main/modes/mfpaint/paint_util.c b/main/modes/mfpaint/paint_util.c new file mode 100644 index 000000000..ccf1ec52d --- /dev/null +++ b/main/modes/mfpaint/paint_util.c @@ -0,0 +1,249 @@ +#include "paint_util.h" + +#include "paint_common.h" + +#include "shapes.h" + +paletteColor_t getContrastingColor(paletteColor_t col) +{ + uint32_t rgb = paletteToRGB(col); + + // TODO I guess this actually won't work at all on 50% gray, or that well on other grays + uint8_t r = 255 - (rgb & 0xFF); + uint8_t g = 255 - ((rgb >> 8) & 0xFF); + uint8_t b = 255 - ((rgb >> 16) & 0xFF); + uint32_t contrastCol = (r << 16) | (g << 8) | (b); + + return RGBtoPalette(contrastCol); +} + +paletteColor_t getContrastingColorBW(paletteColor_t col) +{ + uint32_t rgb = paletteToRGB(col); + uint8_t r = rgb & 0xFF; + uint8_t g = (rgb >> 8) & 0xFF; + uint8_t b = (rgb >> 16) & 0xFF; + + // TODO something with HSL but this pretty much works... + return (r + g + b) / 3 > 76 ? c000 : c555; +} + +void colorReplaceWsg(wsg_t* wsg, paletteColor_t find, paletteColor_t replace) +{ + for (uint16_t i = 0; i < wsg->h * wsg->w; i++) + { + if (wsg->px[i] == find) + { + wsg->px[i] = replace; + } + } +} + +void paintPlotSquareWave(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint16_t waveLength, paletteColor_t col, + int xTr, int yTr, int xScale, int yScale) +{ + uint16_t xDiff = (x0 < x1) ? x1 - x0 : x0 - x1; + uint16_t yDiff = (y0 < y1) ? y1 - y0 : y0 - y1; + + // use the shortest axis as the wave size + uint16_t waveHeight = (xDiff < yDiff) ? xDiff : yDiff; + + if (waveLength == 0) + { + waveLength = waveHeight; + } + + uint16_t x = x0; + uint16_t y = y0; + uint16_t stop, extra; + + int16_t xDir = (x0 < x1) ? 1 : -1; + int16_t yDir = (y0 < y1) ? 1 : -1; + + if (waveHeight < 2 && waveLength < 2) + { + // (a 2xN square wave is just a 2-thick line) + return; + } + + if (xDiff > yDiff) + { + // Horizontal -- waveHeight is on Y axis + PAINT_LOGD("This wave is %d wide and %d tall, which means it will contain %d complete waves plus %d extra", + xDiff, yDiff, xDiff / (waveLength * 2), xDiff % (waveLength * 2)); + extra = xDiff % (waveLength * 2); + stop = x + waveLength * xDir - extra / 2; + + while (x != x1) + { + setPxScaled(x, y, col, xTr, yTr, xScale, yScale); + + if (x == stop) + { + drawLineScaled(x, y, x, y + yDir * waveHeight, col, 0, xTr, yTr, xScale, yScale); + y += yDir * waveHeight; + yDir = -yDir; + stop = x + waveLength * xDir; + } + + x += xDir; + } + } + else + { + // Vertical -- waveHeight is on X axis + extra = yDiff % (waveLength * 2); + stop = y + waveLength * yDir - extra / 2; + + while (y != y1) + { + setPxScaled(x, y, col, xTr, yTr, xScale, yScale); + + if (y == stop) + { + drawLineScaled(x, y, x + xDir * waveHeight, y, col, 0, xTr, yTr, xScale, yScale); + x += xDir * waveHeight; + xDir = -xDir; + stop = y + waveLength * yDir; + } + + y += yDir; + } + } +} + +void drawRectFilled(int x0, int y0, int x1, int y1, paletteColor_t col) +{ + if (x0 >= x1 || y0 >= y1) + { + PAINT_LOGE("Attempted to plot invalid rect drawRectFilled(%d, %d, %d, %d). Returning to avoid segfault", x0, y0, + x1, y1); + return; + } + + fillDisplayArea(x0, y0, x1 - 1, y1 - 1, col); +} + +void drawRectFilledScaled(int x0, int y0, int x1, int y1, paletteColor_t col, int xTr, int yTr, int xScale, int yScale) +{ + fillDisplayArea(xTr + x0 * xScale, yTr + y0 * yScale, xTr + (x1)*xScale, yTr + (y1)*yScale, col); +} + +void paintColorReplace(paintCanvas_t* canvas, paletteColor_t search, paletteColor_t replace) +{ + // super inefficient dumb color replace, maybe do iterated color fill later? + for (uint8_t x = 0; x < canvas->w; x++) + { + for (uint8_t y = 0; y < canvas->h; y++) + { + if (getPxTft(canvas->x + x * canvas->xScale, canvas->y + y * canvas->yScale) == search) + { + setPxScaled(x, y, replace, canvas->x, canvas->y, canvas->xScale, canvas->yScale); + } + } + } +} + +void setPxScaled(int x, int y, paletteColor_t col, int xTr, int yTr, int xScale, int yScale) +{ + drawRectFilledScaled(x, y, x + 1, y + 1, col, xTr, yTr, xScale, yScale); +} + +bool paintDrawWsgTemp(const wsg_t* wsg, pxStack_t* saveTo, uint16_t xOffset, uint16_t yOffset, colorMapFn_t colorSwap) +{ + size_t i = 0; + + // Make sure there's enough space to save the pixels to the stack + if (!maybeGrowPxStack(saveTo, wsg->h * wsg->w)) + { + return false; + } + + for (uint16_t y = 0; y < wsg->h; y++) + { + for (uint16_t x = 0; x < wsg->w; x++, i++) + { + if (wsg->px[i] != cTransparent) + { + if (!pushPx(saveTo, xOffset + x, yOffset + y)) + { + // There wasn't enough space to save the pixel!!! + // This definitely shouldn't have happened because we already + // reserved the space for it... + // Oh well. Stop drawing pixels that we can't take back! + return false; + } + + setPxTft(xOffset + x, yOffset + y, + colorSwap ? colorSwap(getPxTft(xOffset + x, yOffset + y)) : wsg->px[i]); + } + } + } + + return true; +} + +// Calculate the maximum possible [square] scale, given the display's dimensions and the image dimensions, plus any +// margins required +uint8_t paintGetMaxScale(uint16_t imgW, uint16_t imgH, uint16_t xMargin, uint16_t yMargin) +{ + // Prevent infinite loops and overflows + if (xMargin >= TFT_WIDTH || yMargin >= TFT_WIDTH) + { + return 1; + } + + uint16_t maxW = TFT_WIDTH - xMargin; + uint16_t maxH = TFT_HEIGHT - yMargin; + + uint8_t scale = 1; + + while (imgW * (scale + 1) <= maxW && imgH * (scale + 1) <= maxH) + { + scale++; + } + + return scale; +} + +/// @brief Writes the points of a pxStack_t into the given point_t array. +/// @param pxStack The pxStack_t to be converted +/// @param dest A pointer to an array of point_t. Must have room for at least pxStack->index entries. +// void paintConvertPickPoints(const pxStack_t* pxStack, point_t* dest) +// { +// for (size_t i = 0; i < pxStack->index; i++) +// { +// dest[i].x = pxStack->data[i].x; +// dest[i].y = pxStack->data[i].y; +// } +// } + +/// @brief Writes the points of a pxStack_t into the given point_t array, converting them to canvas coordinates +/// @param pxStack The pxStack_t to be converted +/// @param canvas The canvas whose coordinates they should be changed back to +/// @param dest A pointer to an array of point_t. Must have room for at least pxStackSize(pxStack) +void paintConvertPickPointsScaled(const pxStack_t* pxStack, paintCanvas_t* canvas, point_t* dest) +{ + for (size_t i = 0; i < pxStackSize(pxStack); i++) + { + dest[i].x = (pxStack->data[i].x - canvas->x) / canvas->xScale; + dest[i].y = (pxStack->data[i].y - canvas->y) / canvas->yScale; + } +} + +uint16_t canvasToDispX(const paintCanvas_t* canvas, uint16_t x) +{ + return canvas->x + x * canvas->xScale; +} + +uint16_t canvasToDispY(const paintCanvas_t* canvas, uint16_t y) +{ + return canvas->y + y * canvas->yScale; +} + +void swap(uint8_t* a, uint8_t* b) +{ + *a ^= *b; + *b ^= *a; + *a ^= *b; +} diff --git a/main/modes/mfpaint/paint_util.h b/main/modes/mfpaint/paint_util.h new file mode 100644 index 000000000..ac475573a --- /dev/null +++ b/main/modes/mfpaint/paint_util.h @@ -0,0 +1,39 @@ +#ifndef _PAINT_UTIL_H_ +#define _PAINT_UTIL_H_ + +#include +#include + +#include "palette.h" +#include "wsg.h" + +#include "paint_type.h" +#include "px_stack.h" + +paletteColor_t getContrastingColor(paletteColor_t col); +paletteColor_t getContrastingColorBW(paletteColor_t col); + +void colorReplaceWsg(wsg_t* wsg, paletteColor_t find, paletteColor_t replace); + +// Extra drawing functions +bool paintDrawWsgTemp(const wsg_t* wsg, pxStack_t* saveTo, uint16_t x, uint16_t y, colorMapFn_t colorSwap); + +void paintPlotSquareWave(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint16_t waveLength, paletteColor_t col, + int xTr, int yTr, int xScale, int yScale); +void drawRectFilled(int x0, int y0, int x1, int y1, paletteColor_t col); +void drawRectFilledScaled(int x0, int y0, int x1, int y1, paletteColor_t col, int xTr, int yTr, int xScale, int yScale); +void paintColorReplace(paintCanvas_t* canvas, paletteColor_t search, paletteColor_t replace); + +void setPxScaled(int x, int y, paletteColor_t col, int xTr, int yTr, int xScale, int yScale); + +uint8_t paintGetMaxScale(uint16_t imgW, uint16_t imgH, uint16_t xMargin, uint16_t yMargin); + +// void paintConvertPickPoints(const pxStack_t* pxStack, point_t* dest); +void paintConvertPickPointsScaled(const pxStack_t* pxStack, paintCanvas_t* canvas, point_t* dest); + +uint16_t canvasToDispX(const paintCanvas_t* canvas, uint16_t x); +uint16_t canvasToDispY(const paintCanvas_t* canvas, uint16_t y); + +void swap(uint8_t* a, uint8_t* b); + +#endif diff --git a/main/modes/mfpaint/px_stack.c b/main/modes/mfpaint/px_stack.c new file mode 100644 index 000000000..5d5d65eac --- /dev/null +++ b/main/modes/mfpaint/px_stack.c @@ -0,0 +1,186 @@ +#include "px_stack.h" + +#include +#include +#include +#include + +#include "paint_common.h" +#include "paint_util.h" + +bool initPxStack(pxStack_t* pxStack) +{ + pxStack->size = PIXEL_STACK_MIN_SIZE; + PAINT_LOGD("Allocating pixel stack with size %" PRIu32, (uint32_t)pxStack->size); + pxStack->data = malloc(sizeof(pxVal_t) * pxStack->size); + pxStack->index = -1; + + return (pxStack->data != NULL); +} + +void freePxStack(pxStack_t* pxStack) +{ + if (pxStack->data != NULL) + { + free(pxStack->data); + PAINT_LOGD("Freed pixel stack"); + pxStack->size = 0; + pxStack->index = -1; + } +} + +/** + * Ensures that the pixel stack has enough space for `count` additional elements, growing the stack + * if necessary. Retuns true if there is sufficient space, or false if sufficient space could not be + * allocated. + */ +bool maybeGrowPxStack(pxStack_t* pxStack, size_t count) +{ + if (pxStack->index + count >= pxStack->size) + { + size_t newSize = pxStack->size * 2; + + // Ensure the new size can actually accomodate the added count + while (pxStack->index + count >= newSize) + { + newSize *= 2; + } + + PAINT_LOGD("Expanding pixel stack to size %" PRIu32, (uint32_t)newSize); + void* newPtr = realloc(pxStack->data, sizeof(pxVal_t) * newSize); + if (newPtr == NULL) + { + return false; + } + + pxStack->size = newSize; + pxStack->data = newPtr; + } + + return true; +} + +// void maybeShrinkPxStack(pxStack_t* pxStack) +// { +// // If the stack is at least 4 times bigger than it needs to be, shrink it by half +// // (but only if the stack is bigger than the minimum) +// if (pxStack->index >= 0 && pxStack->index * 4 <= pxStack->size && pxStack->size > PIXEL_STACK_MIN_SIZE) +// { +// pxStack->size /= 2; +// PAINT_LOGD("Shrinking pixel stack to %"PRIu32, (uint32_t)pxStack->size); +// pxStack->data = realloc(pxStack->data, sizeof(pxVal_t) * pxStack->size); +// PAINT_LOGD("Done shrinking pixel stack"); +// } +// } + +/** + * The color at the given pixel coordinates is pushed onto the pixel stack, + * along with its coordinates. If the pixel stack is uninitialized, it will + * be allocated. If the pixel stack is full, its size will be doubled. Returns + * true if the pixel was successfully pushed to the stack, or false if adding + * the pixel to the stack failed due to memory constraints. + * + * @brief Pushes a pixel onto the pixel stack so that it can be restored later + * @param x The screen X coordinate of the pixel to save + * @param y The screen Y coordinate of the pixel to save + * @return True if the pixel was pushed successfully, false otherwise + * + */ +bool pushPx(pxStack_t* pxStack, uint16_t x, uint16_t y) +{ + if (!maybeGrowPxStack(pxStack, 1)) + { + return false; + } + + pxStack->index++; + pxStack->data[pxStack->index].x = x; + pxStack->data[pxStack->index].y = y; + pxStack->data[pxStack->index].col = getPxTft(x, y); + + return true; +} + +/** + * Removes the pixel from the top of the stack and draws its color at its coordinates. + * If the stack is already empty, no pixels will be drawn. If the pixel stack's size is + * at least 4 times its number of entries, its size will be halved, at most to the minimum size. + * Returns `true` if a value was popped, and `false` if the stack was empty and no value was popped. + * + * @brief Pops a pixel from the stack and restores it to the screen + * @return `true` if a pixel was popped, and `false` if the stack was empty. + */ +bool popPx(pxStack_t* pxStack) +{ + // Make sure the stack isn't empty + if (pxStack->index >= 0) + { + // Draw the pixel from the top of the stack + setPxTft(pxStack->data[pxStack->index].x, pxStack->data[pxStack->index].y, pxStack->data[pxStack->index].col); + pxStack->index--; + + // Is this really necessary? The stack empties often so maybe it's better not to reallocate constantly + // maybeShrinkPxStack(pxStack); + + return true; + } + + return false; +} + +bool peekPx(const pxStack_t* pxStack, pxVal_t* dest) +{ + if (pxStack->index >= 0) + { + *dest = pxStack->data[pxStack->index]; + return true; + } + + return false; +} + +bool getPx(const pxStack_t* pxStack, size_t pos, pxVal_t* dest) +{ + if (pos <= pxStack->index) + { + *dest = pxStack->data[pos]; + return true; + } + + return false; +} + +bool dropPx(pxStack_t* pxStack) +{ + if (pxStack->index >= 0) + { + pxStack->index--; + return true; + } + + return false; +} + +size_t pxStackSize(const pxStack_t* pxStack) +{ + return pxStack->index + 1; +} + +bool pushPxScaled(pxStack_t* pxStack, int x, int y, int xTr, int yTr, int xScale, int yScale) +{ + return pushPx(pxStack, xTr + x * xScale, yTr + y * yScale); +} + +bool popPxScaled(pxStack_t* pxStack, int xScale, int yScale) +{ + pxVal_t px; + if (peekPx(pxStack, &px)) + { + drawRectFilled(px.x, px.y, px.x + xScale + 1, px.y + yScale + 1, px.col); + dropPx(pxStack); + + return true; + } + + return false; +} diff --git a/main/modes/mfpaint/px_stack.h b/main/modes/mfpaint/px_stack.h new file mode 100644 index 000000000..ec0df3dae --- /dev/null +++ b/main/modes/mfpaint/px_stack.h @@ -0,0 +1,45 @@ +#ifndef _PX_STACK_H_ +#define _PX_STACK_H_ + +#include +#include +#include + +#include "palette.h" + +#define PIXEL_STACK_MIN_SIZE 2 + +/// @brief Represents the value of a pixel and its screen coordinates +typedef struct +{ + uint16_t x, y; + paletteColor_t col; +} pxVal_t; + +/// @brief A structure for storing an unbounded number of pixels and their color. +typedef struct +{ + // Pointer to the first of pixel coordinates/values, heap-allocated + pxVal_t* data; + + // The number of pxVal_t entries currently allocated for the stack + size_t size; + + // The index of the value on the top of the stack + int32_t index; +} pxStack_t; + +bool initPxStack(pxStack_t* pxStack); +void freePxStack(pxStack_t* pxStack); +bool maybeGrowPxStack(pxStack_t* pxStack, size_t count); +// void maybeShrinkPxStack(pxStack_t* pxStack); +bool pushPx(pxStack_t* pxStack, uint16_t x, uint16_t y); +bool popPx(pxStack_t* pxStack); +bool peekPx(const pxStack_t* pxStack, pxVal_t* dest); +bool getPx(const pxStack_t* pxStack, size_t pos, pxVal_t* dest); +bool dropPx(pxStack_t* pxStack); +size_t pxStackSize(const pxStack_t* pxStack); +bool pushPxScaled(pxStack_t* pxStack, int x, int y, int xTr, int yTr, int xScale, int yScale); +bool popPxScaled(pxStack_t* pxStack, int xScale, int yScale); + +#endif diff --git a/main/modes/pushy/pushy.c b/main/modes/pushy/pushy.c new file mode 100644 index 000000000..f4e3ca5a8 --- /dev/null +++ b/main/modes/pushy/pushy.c @@ -0,0 +1,524 @@ +/** + * @file pushy.c + * @author Brycey92 + * @brief A port of Socks' Pushy Kawaii 2 + * @link https://cults3d.com/en/3d-model/game/pushy-kawaii-v2 + * @date 2023-09-07 + */ + +//============================================================================== +// Includes +//============================================================================== + +#include +#include +#include + +#include "color_utils.h" +#include "esp_random.h" +#include "esp_timer.h" +#include "hdw-led.h" +#include "hdw-nvs.h" + +#include "pushy.h" + +//============================================================================== +// Defines +//============================================================================== + +// clang-format off + +#define LOGFIRE false +#define LOGPUSHY false + +#define NUM_DIGITS 8 +#define NUM_PUSHY_COLORS 11 // 0-9 and "off" + +#define IDLE_SECONDS_UNTIL_SAVE 3 +#define SAVE_AT_MOD 100 + +#define SHUFFLE_AT_MOD 1000 + +// reserve a color for white, one for "off"/grey, and evenly spread the remaining colors across the rainbow +#define HUE_STEP (255 / (NUM_PUSHY_COLORS - 2)) +// reserve a color for "off"/grey, and evenly spread the remaining colors across the rainbow +#define RAINBOW_HUE_STEP (255 / (NUM_PUSHY_COLORS - 1)) +#define SATURATION 255 +#define BRIGHTNESS 255 +// clang-format on + +#define EFFECT_MAX 200 + +#define FIRE_TIMER_MS 100 +#define FIREWINDOWS 100 + +//============================================================================== +// Enums +//============================================================================== + +//============================================================================== +// Structs +//============================================================================== + +typedef struct +{ + // clang-format off + font_t sevenSegment; ///< The font used in the game + + char eights[NUM_DIGITS + 1]; ///< A string of '8's to draw behind the score as "unlit" seven-segment displays + uint16_t eightsWidth; ///< The width of the string of '8's, in pixels + + uint32_t counter; ///< The score for the game + uint32_t lastSaveCounter; ///< The last score that was saved + + uint32_t allFireCounts[FIREWINDOWS]; + uint32_t fireCounter; + uint32_t fireWindowCount; + int64_t fireWindowStartUs; + int64_t buttonPushedUs; ///< Microseconds since the last button push + uint16_t btnState; ///< The button state + + int64_t rainbowTimer; ///< 0 if no digits should be rainbow, or EFFECT_MAX if any digits should + int64_t weedTimer; ///< 0 if no digits should be weed colored, or EFFECT_MAX if any digits should + bool rainbowDigits[NUM_DIGITS]; ///< A bitmap of digits that should be rainbow, from most significant digit at [0] to least significant digit at [NUM_DIGITS] + bool weedDigits[NUM_DIGITS]; ///< A bitmap of digits that should be weed colored, from most significant digit at [0] to least significant digit at [NUM_DIGITS] + float weedHue; ///< The hue to display on digits that are weed colored + + paletteColor_t colors[NUM_PUSHY_COLORS]; ///< Colors for each digit 0-9 + uint8_t rainbowHues[NUM_PUSHY_COLORS]; ///< Hues to display on digits that are rainbow + + led_t boxleds[CONFIG_NUM_LEDS]; + // clang-format on +} pushy_t; + +//============================================================================== +// Function Prototypes +//============================================================================== + +static void pushyMainLoop(int64_t elapsedUs); +static void pushyEnterMode(void); +static void pushyExitMode(void); +static void pushyBackgroundDrawCallback(int16_t x, int16_t y, int16_t w, int16_t h, int16_t up, int16_t upNum); + +static void saveMemory(void); +static void shuffleColors(void); +static void readButton(void); +static void displayCounter(char const* counterStr); +static void checkSubStr(char const* counterStr, char const* const subStr, bool digitBitmap[NUM_DIGITS], int64_t* timer); +static void checkRainbow(char const* counterStr); +static void checkWeed(char const* counterStr); +static void updateEffects(char const* counterStr); +static uint32_t getFireCount(void); +static void displayFire(void); +static void updateFire(void); +void showDigit(uint8_t number, uint8_t colorIndex, uint8_t digitIndexFromLeastSignificant); + +//============================================================================== +// Strings +//============================================================================== + +/* Design Pattern! + * These strings are all declared 'const' because they do not change, so that they are placed in ROM, not RAM. + * Lengths are not explicitly given so the compiler can figure it out. + */ + +static const char pushyName[] = "Pushy Kawaii Go"; +static const char pushyCounterKey[] = "pk_counter"; +static const char rainbowStr[] = "69"; +static const char weedStr[] = "420"; + +//============================================================================== +// Variables +//============================================================================== + +/// The Swadge mode for Pushy +swadgeMode_t pushyMode = { + .modeName = pushyName, + .wifiMode = NO_WIFI, + .overrideUsb = false, + .usesAccelerometer = false, + .usesThermometer = false, + .fnEnterMode = pushyEnterMode, + .fnExitMode = pushyExitMode, + .fnMainLoop = pushyMainLoop, + .fnAudioCallback = NULL, + .fnBackgroundDrawCallback = pushyBackgroundDrawCallback, + .fnEspNowRecvCb = NULL, + .fnEspNowSendCb = NULL, + .fnAdvancedUSB = NULL, +}; + +/// All state information for the Pushy mode. This whole struct is calloc()'d and free()'d so that Pushy is only +/// using memory while it is being played +pushy_t* pushy = NULL; + +//============================================================================== +// Functions +//============================================================================== + +/** + * @brief Enter Pushy mode, allocate required memory, and initialize required variables + * + */ +static void pushyEnterMode(void) +{ + // Allocate and clear all memory for this mode. All the variables are contained in a single struct for convenience. + // calloc() is used instead of malloc() because calloc() also initializes the allocated memory to zeros. + pushy = calloc(1, sizeof(pushy_t)); + + // Load a font + loadFont("seven_segment.font", &pushy->sevenSegment, false); + + // Initialize string for "unlit" seven-segment displays + memset(pushy->eights, '8', NUM_DIGITS); + pushy->eights[NUM_DIGITS] = 0; + pushy->eightsWidth = textWidth(&pushy->sevenSegment, pushy->eights); + + // Load score from NVS + readNvs32(pushyCounterKey, (int32_t*)&pushy->counter); + + // Initialize fire variables + pushy->fireWindowStartUs = esp_timer_get_time(); + + // Initialize weed and rainbow effect variables + pushy->rainbowTimer = EFFECT_MAX; + pushy->weedTimer = EFFECT_MAX; + + // Initialize default color values + // clang-format off + pushy->colors[0] = paletteHsvToHex( 0, 0, BRIGHTNESS); // white + pushy->colors[1] = paletteHsvToHex(HUE_STEP * 0, SATURATION, BRIGHTNESS); // red + pushy->colors[2] = paletteHsvToHex(HUE_STEP * 1, SATURATION, BRIGHTNESS); // orange + pushy->colors[3] = paletteHsvToHex(HUE_STEP * 2, SATURATION, BRIGHTNESS); // yellow + pushy->colors[4] = paletteHsvToHex(HUE_STEP * 3, SATURATION, BRIGHTNESS); // lime green + pushy->colors[5] = paletteHsvToHex(HUE_STEP * 4, SATURATION, BRIGHTNESS); // green + pushy->colors[6] = paletteHsvToHex(HUE_STEP * 5, SATURATION, BRIGHTNESS); // aqua-ish + pushy->colors[7] = c025; //paletteHsvToHex(HUE_STEP * 6, SATURATION, BRIGHTNESS); // blue + pushy->colors[8] = paletteHsvToHex(HUE_STEP * 7, SATURATION, BRIGHTNESS); // purpley + pushy->colors[9] = paletteHsvToHex(HUE_STEP * 8, SATURATION, BRIGHTNESS); // pinkish + pushy->colors[10] = paletteHsvToHex(0, 0, 55); // grey + + pushy->rainbowHues[0] = RAINBOW_HUE_STEP * 0; + pushy->rainbowHues[1] = RAINBOW_HUE_STEP * 1; + pushy->rainbowHues[2] = RAINBOW_HUE_STEP * 2; + pushy->rainbowHues[3] = RAINBOW_HUE_STEP * 3; + pushy->rainbowHues[4] = RAINBOW_HUE_STEP * 4; + pushy->rainbowHues[5] = RAINBOW_HUE_STEP * 5; + pushy->rainbowHues[6] = RAINBOW_HUE_STEP * 6; + pushy->rainbowHues[7] = RAINBOW_HUE_STEP * 7; + pushy->rainbowHues[8] = RAINBOW_HUE_STEP * 8; + pushy->rainbowHues[9] = RAINBOW_HUE_STEP * 9; + pushy->rainbowHues[10] = RAINBOW_HUE_STEP * 10; // never actually used, as this is redirected to pushy->colors[10] + // clang-format on + + shuffleColors(); +} + +/** + * This function is called when the mode is exited. It deinitializes variables and frees all memory. + */ +static void pushyExitMode(void) +{ + // Save score to NVS + if (pushy->lastSaveCounter != pushy->counter) + { + writeNvs32(pushyCounterKey, (int32_t)pushy->counter); + } + + // Free the font + freeFont(&pushy->sevenSegment); + + // Free everything else + free(pushy); +} + +/** + * @brief This function is called periodically and frequently. It will either draw the menu or play the game, depending + * on which screen is currently being displayed + * + * @param elapsedUs The time that has elapsed since the last call to this function, in microseconds + */ +static void pushyMainLoop(int64_t elapsedUs) +{ + pushy->buttonPushedUs += elapsedUs; + + readButton(); + + // If score has changed, save if last input was longer ago than our threshold or if score is a multiple of 100 + if (pushy->lastSaveCounter != pushy->counter && pushy->buttonPushedUs > IDLE_SECONDS_UNTIL_SAVE * 1000 * 1000) + { + saveMemory(); + } + + // Draw "unlit" seven-segment displays + drawText(&pushy->sevenSegment, pushy->colors[NUM_PUSHY_COLORS - 1], pushy->eights, + (TFT_WIDTH - pushy->eightsWidth) / 2, (TFT_HEIGHT - pushy->sevenSegment.height) / 2); + + // Draw "lit" segments + char counterStr[NUM_DIGITS + 1]; + snprintf(counterStr, NUM_DIGITS + 1, "%*" PRIu32, NUM_DIGITS, pushy->counter); + + updateEffects(counterStr); + updateFire(); + + displayCounter(counterStr); + displayFire(); + + setLeds(pushy->boxleds, CONFIG_NUM_LEDS); +} + +/** + * This function is called when the display driver wishes to update a + * section of the display. + * + * @param disp The display to draw to + * @param x the x coordinate that should be updated + * @param y the x coordinate that should be updated + * @param w the width of the rectangle to be updated + * @param h the height of the rectangle to be updated + * @param up update number + * @param numUp update number denominator + */ +static void pushyBackgroundDrawCallback(int16_t x, int16_t y, int16_t w, int16_t h, int16_t up, int16_t upNum) +{ + // Use TURBO drawing mode to draw individual pixels fast + SETUP_FOR_TURBO(); + + // Draw a grid + for (int16_t yp = y; yp < y + h; yp++) + { + for (int16_t xp = x; xp < x + w; xp++) + { + TURBO_SET_PIXEL(xp, yp, c000); + } + } +} + +// Save score to NVS +static void saveMemory(void) +{ + writeNvs32(pushyCounterKey, (int32_t)pushy->counter); + pushy->lastSaveCounter = pushy->counter; +} + +static void shuffleColors(void) +{ + for (uint8_t i = 0; i < NUM_PUSHY_COLORS - 1; i++) + { + uint8_t n = esp_random() % (NUM_PUSHY_COLORS - 1); +#if LOGPUSHY + printf("gonna swap the next two colors: %" PRIu8 ", %" PRIu8 "\n", i, n); +#endif + + paletteColor_t temp = pushy->colors[n]; + pushy->colors[n] = pushy->colors[i]; + pushy->colors[i] = temp; + } +} + +static void readButton(void) +{ + buttonEvt_t evt = {0}; + while (checkButtonQueueWrapper(&evt)) + { + // Save the button state + pushy->btnState = evt.state; + + // Check if the A button was pressed + if (evt.down && (PB_A == evt.button)) + { + pushy->counter++; + pushy->fireCounter++; + pushy->buttonPushedUs = 0; + if (pushy->counter % SAVE_AT_MOD == 0) + { + saveMemory(); + } + if (pushy->counter % SHUFFLE_AT_MOD == 0) + { + shuffleColors(); + } + } + } +} + +static void displayCounter(char const* counterStr) +{ +#if LOGPUSHY + printf("Displaying counter\n"); +#endif + + for (unsigned int i = 0; i < NUM_DIGITS; i++) + { + // As i increases, we move from least-significant digit to most-significant digit + if ((counterStr[NUM_DIGITS - 1 - i]) != ' ') + { + int c = counterStr[NUM_DIGITS - 1 - i] - '0'; + showDigit(c, c, i); + } + } +} + +static void checkSubStr(char const* counterStr, char const* const subStr, bool digitBitmap[NUM_DIGITS], int64_t* timer) +{ + memset(digitBitmap, false, NUM_DIGITS); + + char const* subStrInCounterStr = strstr(counterStr, subStr); + if (subStrInCounterStr == NULL) + { + *timer = EFFECT_MAX; + return; + } + + uint8_t startDigit = subStrInCounterStr - counterStr; + for (uint8_t i = 0; i < strlen(subStr); i++) + { + digitBitmap[startDigit + i] = true; + } + + *timer = 0; +} + +static void checkRainbow(char const* counterStr) +{ + checkSubStr(counterStr, rainbowStr, pushy->rainbowDigits, &pushy->rainbowTimer); +} + +static void checkWeed(char const* counterStr) +{ + checkSubStr(counterStr, weedStr, pushy->weedDigits, &pushy->weedTimer); +} + +static void updateEffects(char const* counterStr) +{ +#if LOGPUSHY + printf("Updating effects\n"); +#endif + checkRainbow(counterStr); + checkWeed(counterStr); + + if (pushy->rainbowTimer < EFFECT_MAX) + { + for (int i = 0; i < NUM_PUSHY_COLORS; i++) + { + pushy->rainbowHues[i] = (pushy->rainbowHues[i] + 2 % 255); + } + } + + if (pushy->weedTimer < EFFECT_MAX) + { + pushy->weedHue = 105; + // pushy->weedHue -= 0.25; + // pushy->weedHue = MAX(weedHue, 24); + } +} + +static uint32_t getFireCount(void) +{ + uint32_t totalFire = 0; + + for (uint8_t i = 0; i < FIREWINDOWS; i++) + { + totalFire += pushy->allFireCounts[i]; + } + +#if LOGFIRE + printf("Fire subs: "); + + for (uint8_t i = 0; i < FIREWINDOWS; i++) + { + printf("%" PRIu32 " ", pushy->allFireCounts[i]); + } + printf("\n%" PRIu32 "\n", totalFire); +#endif + + return totalFire; +} + +static void displayFire(void) +{ +#if LOGPUSHY + printf("Displaying fire\n"); +#endif + + // for (int i = 0; i < CONFIG_NUM_LEDS; i++) + // { + // pushy->boxleds[i] = LedEHSVtoHEXhelper(0, 200, 200); + // } + // setLeds(pushy->boxleds, CONFIG_NUM_LEDS); + // return; + + int count = getFireCount(); + + led_t color = LedEHSVtoHEXhelper((uint8_t)(count / 2.5), 255, 250, true); + + for (int i = 0; i < CONFIG_NUM_LEDS; i++) + { + if (count > 5) + { + pushy->boxleds[i] = color; + } + else + { + pushy->boxleds[i] = LedEHSVtoHEXhelper(0, 0, 10, true); + } + } +} + +static void updateFire(void) +{ + int64_t currentUs = esp_timer_get_time(); + if (currentUs - pushy->fireWindowStartUs > FIRE_TIMER_MS * 1000) // defines how long between checks of the windows + { + pushy->allFireCounts[pushy->fireWindowCount] = pushy->fireCounter; // how many presses in the current window + pushy->fireCounter = 0; + pushy->fireWindowCount = (pushy->fireWindowCount + 1) % FIREWINDOWS; // this rotates us through the array + pushy->fireWindowStartUs = currentUs; + +#if LOGPUSHY + printf("Fire count: %" PRIu32 "\n", getFireCount()); +#endif + } +} + +void showDigit(uint8_t number, uint8_t colorIndex, uint8_t digitIndexFromLeastSignificant) +{ +#if LOGPUSHY + printf("showing a digit\n"); +#endif + + // Convert the number to a string + paletteColor_t color; + char numberAsStr[4]; + snprintf(numberAsStr, sizeof(numberAsStr), "%1" PRIu8, number); + + // Apply weed and rainbow effects, or if no effects, get the current color for this digit + if (pushy->weedDigits[NUM_DIGITS - 1 - digitIndexFromLeastSignificant]) + { + color = paletteHsvToHex((int)pushy->weedHue, SATURATION, BRIGHTNESS); +#if LOGPUSHY + printf("weed digit at %" PRIu8 "\n", digitIndexFromLeastSignificant); + printf("weed timer is %" PRIi64 "\n", pushy->weedTimer); +#endif + } + else if (pushy->rainbowDigits[NUM_DIGITS - 1 - digitIndexFromLeastSignificant] + && colorIndex != NUM_PUSHY_COLORS - 1) + { + color = paletteHsvToHex(pushy->rainbowHues[0], SATURATION, BRIGHTNESS); +#if LOGPUSHY + printf("rainbow digit at %" PRIu8 "\n", digitIndexFromLeastSignificant); + printf("rainbow timer is %" PRIi64 "\n", pushy->rainbowTimer); +#endif + } + else + { + color = pushy->colors[colorIndex]; + } + + // Draw the digit to the screen + uint16_t digitWidth = textWidth(&pushy->sevenSegment, "8"); + drawText(&pushy->sevenSegment, color, numberAsStr, + (TFT_WIDTH - pushy->eightsWidth) / 2 + + ((digitWidth + 1) * (NUM_DIGITS - 1 - digitIndexFromLeastSignificant)), + (TFT_HEIGHT - pushy->sevenSegment.height) / 2); +} diff --git a/main/modes/pushy/pushy.h b/main/modes/pushy/pushy.h new file mode 100644 index 000000000..9579d3700 --- /dev/null +++ b/main/modes/pushy/pushy.h @@ -0,0 +1,8 @@ +#ifndef _PUSHY_H_ +#define _PUSHY_H_ + +#include "swadge2024.h" + +extern swadgeMode_t pushyMode; + +#endif diff --git a/main/modes/quickSettings/quickSettings.c b/main/modes/quickSettings/quickSettings.c index ec6e094af..6c711a882 100644 --- a/main/modes/quickSettings/quickSettings.c +++ b/main/modes/quickSettings/quickSettings.c @@ -240,6 +240,7 @@ static void quickSettingsExitMode(void) freeWsg(&quickSettings->iconTftOff); free(quickSettings); + quickSettings = NULL; } /** @@ -309,8 +310,12 @@ static void quickSettingsMainLoop(int64_t elapsedUs) quickSettings->menu = menuButton(quickSettings->menu, evt); } - // Draw the menu - drawMenuQuickSettings(quickSettings->menu, quickSettings->renderer, elapsedUs); + // If the button press didn't cause the menu to deinit + if (NULL != quickSettings) + { + // Draw the menu + drawMenuQuickSettings(quickSettings->menu, quickSettings->renderer, elapsedUs); + } } /** diff --git a/main/modes/ray/fp_math.c b/main/modes/ray/fp_math.c new file mode 100644 index 000000000..90c251ee5 --- /dev/null +++ b/main/modes/ray/fp_math.c @@ -0,0 +1,273 @@ +//============================================================================== +// Includes +//============================================================================== + +#include "fp_math.h" + +//============================================================================== +// Lookup Tables +//============================================================================== + +/** + * @brief q24_8 lookup table to convert from a ratio between X and Y vectors to the normalized length of the X vector + * + * Equivalent to "f(x) = x / sqrt((x * x) + 1)" + * Inputs are in the range [0x00, 0xFF], equivalent to [0, 1] + * Outputs are in the range [0x00, 0xB5], equivalent to [0, sqrt(2)] + */ +static const uint8_t ratioToXLut[] = { + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10, 0x11, 0x12, + 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, + 0x26, 0x27, 0x28, 0x28, 0x29, 0x2A, 0x2B, 0x2C, 0x2D, 0x2E, 0x2F, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, + 0x38, 0x39, 0x39, 0x3A, 0x3B, 0x3C, 0x3D, 0x3E, 0x3F, 0x40, 0x41, 0x42, 0x43, 0x44, 0x44, 0x45, 0x46, 0x47, 0x48, + 0x49, 0x4A, 0x4B, 0x4B, 0x4C, 0x4D, 0x4E, 0x4F, 0x50, 0x51, 0x52, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x57, 0x58, + 0x59, 0x5A, 0x5B, 0x5C, 0x5C, 0x5D, 0x5E, 0x5F, 0x60, 0x60, 0x61, 0x62, 0x63, 0x64, 0x64, 0x65, 0x66, 0x67, 0x67, + 0x68, 0x69, 0x6A, 0x6A, 0x6B, 0x6C, 0x6D, 0x6D, 0x6E, 0x6F, 0x70, 0x70, 0x71, 0x72, 0x72, 0x73, 0x74, 0x75, 0x75, + 0x76, 0x77, 0x77, 0x78, 0x79, 0x79, 0x7A, 0x7B, 0x7C, 0x7C, 0x7D, 0x7E, 0x7E, 0x7F, 0x7F, 0x80, 0x81, 0x81, 0x82, + 0x83, 0x83, 0x84, 0x85, 0x85, 0x86, 0x86, 0x87, 0x88, 0x88, 0x89, 0x89, 0x8A, 0x8B, 0x8B, 0x8C, 0x8C, 0x8D, 0x8E, + 0x8E, 0x8F, 0x8F, 0x90, 0x90, 0x91, 0x92, 0x92, 0x93, 0x93, 0x94, 0x94, 0x95, 0x95, 0x96, 0x96, 0x97, 0x98, 0x98, + 0x99, 0x99, 0x9A, 0x9A, 0x9B, 0x9B, 0x9C, 0x9C, 0x9D, 0x9D, 0x9E, 0x9E, 0x9F, 0x9F, 0xA0, 0xA0, 0xA0, 0xA1, 0xA1, + 0xA2, 0xA2, 0xA3, 0xA3, 0xA4, 0xA4, 0xA5, 0xA5, 0xA6, 0xA6, 0xA6, 0xA7, 0xA7, 0xA8, 0xA8, 0xA9, 0xA9, 0xA9, 0xAA, + 0xAA, 0xAB, 0xAB, 0xAC, 0xAC, 0xAC, 0xAD, 0xAD, 0xAE, 0xAE, 0xAE, 0xAF, 0xAF, 0xAF, 0xB0, 0xB0, 0xB1, 0xB1, 0xB1, + 0xB2, 0xB2, 0xB2, 0xB3, 0xB3, 0xB4, 0xB4, 0xB4, 0xB5, 0xB5, +}; + +/** + * @brief q24_8 Lookup table to find the complimentary Y vector for a normalized X vector + * + * Equivalent to "f(x) = sqrt(1 - (x * x))" + * Inputs are in the range [0x00, 0xB5], equivalent to [0, sqrt(2)] (outputs of ratioToXLut[]) + * Outputs are in the range [0xB5, 0x100], equivalent to [0, 1] + * + * Values of 0x00 in this table must be treated as 0x0100. This is so values can be 8 bit + */ +static const uint8_t complNormLut[] = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFD, 0xFD, + 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFC, 0xFC, 0xFC, 0xFC, 0xFC, 0xFB, 0xFB, 0xFB, 0xFB, 0xFB, 0xFA, 0xFA, 0xFA, 0xFA, + 0xFA, 0xF9, 0xF9, 0xF9, 0xF9, 0xF8, 0xF8, 0xF8, 0xF8, 0xF7, 0xF7, 0xF7, 0xF7, 0xF6, 0xF6, 0xF6, 0xF5, 0xF5, 0xF5, + 0xF4, 0xF4, 0xF4, 0xF4, 0xF3, 0xF3, 0xF3, 0xF2, 0xF2, 0xF1, 0xF1, 0xF1, 0xF0, 0xF0, 0xF0, 0xEF, 0xEF, 0xEF, 0xEE, + 0xEE, 0xED, 0xED, 0xEC, 0xEC, 0xEC, 0xEB, 0xEB, 0xEA, 0xEA, 0xE9, 0xE9, 0xE9, 0xE8, 0xE8, 0xE7, 0xE7, 0xE6, 0xE6, + 0xE5, 0xE5, 0xE4, 0xE4, 0xE3, 0xE3, 0xE2, 0xE2, 0xE1, 0xE1, 0xE0, 0xDF, 0xDF, 0xDE, 0xDE, 0xDD, 0xDD, 0xDC, 0xDB, + 0xDB, 0xDA, 0xDA, 0xD9, 0xD8, 0xD8, 0xD7, 0xD6, 0xD6, 0xD5, 0xD4, 0xD4, 0xD3, 0xD2, 0xD2, 0xD1, 0xD0, 0xCF, 0xCF, + 0xCE, 0xCD, 0xCC, 0xCC, 0xCB, 0xCA, 0xC9, 0xC9, 0xC8, 0xC7, 0xC6, 0xC5, 0xC5, 0xC4, 0xC3, 0xC2, 0xC1, 0xC0, 0xBF, + 0xBF, 0xBE, 0xBD, 0xBC, 0xBB, 0xBA, 0xB9, 0xB8, 0xB7, 0xB6, 0xB5, +}; + +//============================================================================== +// Functions +//============================================================================== + +/** + * @brief Quickly normalize a q24_8 vector, in-place + * + * @param xp The X component of the vector, normalized in-place + * @param yp The Y component of the vector, normalized in-place + */ +void fastNormVec(q24_8* xp, q24_8* yp) +{ + // Save pointer values to local + q24_8 x = *xp; + q24_8 y = *yp; + // Local variables for the normalized length + q24_8 nx, ny; + + // Adjust signs to be positive for LUTs + bool xIsPos = true; + if (x < 0) + { + xIsPos = false; + x = -x; + } + bool yIsPos = true; + if (y < 0) + { + yIsPos = false; + y = -y; + } + + // Check special cases + if (x == 0) + { + nx = 0; + ny = TO_FX(1); + } + else if (y == 0) + { + nx = TO_FX(1); + ny = 0; + } + // Do ratio math + else if (x <= y) + { + // x is <= y, so the ratio will always be <= 1 (0x0100 or less in q24_8) + q24_8 ratio = DIV_FX(x, y); + nx = ratioToXLut[ratio]; + + // Treat 0x00 as 0x0100 + if (complNormLut[nx]) + { + ny = complNormLut[nx]; + } + else + { + ny = 0x100; + } + } + else if (y < x) + { + // y is less than x, so the ratio will always be less than 1 (8 bits or fewer) + q24_8 ratio = DIV_FX(y, x); + ny = ratioToXLut[ratio]; + + // Treat 0x00 as 0x0100 + if (complNormLut[ny]) + { + nx = complNormLut[ny]; + } + else + { + nx = 0x100; + } + } + + // Reapply signs + if (!xIsPos) + { + nx = -(nx); + } + if (!yIsPos) + { + ny = -(ny); + } + + // Return values + *xp = nx; + *yp = ny; +} + +#ifdef LUT_GEN_FUNCS + +typedef struct +{ + int32_t x; + int32_t y; + float err; +} testResult_t; + +/** + * @brief Comparator function to sort testResult_t by error + * + * @param p1 A testResult_t to compare + * @param p2 A testResult_t to compare + * @return A number less than, equal to, or greater than 0 if p1' error greater than, equal to, or less than p2's error + */ +int comparator(const void* p1, const void* p2) +{ + if (((testResult_t*)p1)->err < ((testResult_t*)p2)->err) + { + return 1; + } + else if (((testResult_t*)p1)->err > ((testResult_t*)p2)->err) + { + return -1; + } + else + { + return ((testResult_t*)p1)->x - ((testResult_t*)p2)->x; + } +} + +/** + * @brief Main function to generate LUTs and validate errors between floating and fixed point values + * + * @param argc unused + * @param argv unused + * @return 0 + */ +int main(int argc __attribute__((unused)), char** argv __attribute__((unused))) +{ + #ifdef PRINT_LUTS + printf("uint8_t ratioToXLut[] = {\n"); + for (int i = 0; i <= TO_FX(1); i++) + { + printf(" 0x%02X,\n", floatToFix(ratioToX(fixToFloat(i)))); + } + printf("};\n\n"); + + printf("uint16_t complNormLut[] = {\n"); + for (int i = 0; i <= ratioToXLut[sizeof(ratioToXLut) - 1]; i++) + { + printf(" 0x%04X,\n", floatToFix(complNorm(fixToFloat(i)))); + } + printf("};\n\n"); + #endif + + #define TEST_RANGE 1000 + // Allocate space for test results + testResult_t* results = calloc(4 * TEST_RANGE * TEST_RANGE, sizeof(testResult_t)); + // Test all vectors in a 1000 x 1000 range + for (int x = -TEST_RANGE; x < TEST_RANGE; x++) + { + // Ignore symmetric vectors + for (int y = -TEST_RANGE; y < TEST_RANGE; y++) + { + if (!(x == 0 && y == 0)) + { + // Slow, accurate normalization + float len = sqrt((x * x) + (y * y)); + float fnx = x / len; + float fny = y / len; + + // Fast, inaccurate normalization + q24_8 ox = TO_FX(x); + q24_8 oy = TO_FX(y); + fastNormVec(&ox, &oy); + + // Error + float ex = fnx - fixToFloat(ox); + float ey = fny - fixToFloat(oy); + float errMag = sqrt(ex * ex + ey * ey); + + // Save the result to sort later + results[(x + TEST_RANGE) * (2 * TEST_RANGE) + (y + TEST_RANGE)].x = x; + results[(x + TEST_RANGE) * (2 * TEST_RANGE) + (y + TEST_RANGE)].y = y; + results[(x + TEST_RANGE) * (2 * TEST_RANGE) + (y + TEST_RANGE)].err = errMag; + } + } + } + + // Sort all tests by error + qsort(results, TEST_RANGE * TEST_RANGE, sizeof(testResult_t), comparator); + + // Print the 10 worst offenders + for (int i = 0; i < 10; i++) + { + int32_t x = results[i].x; + int32_t y = results[i].y; + + // Slow, accurate normalization + float len = sqrt((x * x) + (y * y)); + float fnx = x / len; + float fny = y / len; + + // Fast, inaccurate normalization + q24_8 ox = TO_FX(x); + q24_8 oy = TO_FX(y); + fastNormVec(&ox, &oy); + + // Error + float ex = fnx - fixToFloat(ox); + float ey = fny - fixToFloat(oy); + float errMag = sqrt(ex * ex + ey * ey); + + printf("(%4d, %4d) => (%8.05f, %8.05f) == (%8.05f, %8.05f), err: %.05f\n", x, y, fnx, fny, fixToFloat(ox), + fixToFloat(oy), errMag); + } + + // Clean up + free(results); + return 0; +} + +#endif diff --git a/main/modes/ray/fp_math.h b/main/modes/ray/fp_math.h new file mode 100644 index 000000000..3b442b346 --- /dev/null +++ b/main/modes/ray/fp_math.h @@ -0,0 +1,90 @@ +#ifndef _FP_MATH_H_ +#define _FP_MATH_H_ + +#include +#include + +typedef int32_t q24_8; // 24 bits integer, 8 bits fraction +typedef int32_t q16_16; // 16 bits integer, 16 bits fraction +typedef int32_t q8_24; // 8 bits integer, 24 bits fraction + +#define FRAC_BITS 8 +#define Q24_8_DECI_MASK ((1 << FRAC_BITS) - 1) +#define Q24_8_WHOLE_MASK (~Q24_8_DECI_MASK) + +#define Q16_16_FRAC_BITS 16 +#define Q16_16_DECI_MASK ((1 << Q16_16_FRAC_BITS) - 1) +#define Q16_16_WHOLE_MASK (~Q16_16_DECI_MASK) + +#define Q8_24_FRAC_BITS 24 +#define Q8_24_DECI_MASK ((1 << Q8_24_FRAC_BITS) - 1) +#define Q8_24_WHOLE_MASK (~Q8_24_DECI_MASK) + +//============================================================================== +// Fixed Point Math Functions +//============================================================================== + +void fastNormVec(q24_8* xp, q24_8* yp); + +// Switch to use macros or inline functions +#define FP_MATH_DEFINES + +#ifdef FP_MATH_DEFINES + + #define TO_FX(in) ((in) << FRAC_BITS) + #define FROM_FX(in) ((in) >> FRAC_BITS) + #define ADD_FX(a, b) ((a) + (b)) + #define SUB_FX(a, b) ((a) - (b)) + #define MUL_FX(a, b) (((a) * (b)) >> FRAC_BITS) + #define DIV_FX(a, b) (((a) << FRAC_BITS) / (b)) + #define FLOOR_FX(a) ((a) & (~((1 << FRAC_BITS) - 1))) + #define TO_FX_FRAC(num, denom) DIV_FX(num, denom) + +#else + +static inline q24_8 TO_FX(uint32_t in) +{ + return (q24_8)(in * (1 << FRAC_BITS)); +} + +static inline int32_t FROM_FX(q24_8 in) +{ + return in / (1 << FRAC_BITS); +} + +static inline q24_8 ADD_FX(q24_8 a, q24_8 b) +{ + return a + b; +} + +static inline q24_8 SUB_FX(q24_8 a, q24_8 b) +{ + return a - b; +} + +static inline q24_8 MUL_FX(q24_8 a, q24_8 b) +{ + // could be simpler without rounding + return ((a * b) + (1 << (FRAC_BITS - 1))) / (1 << FRAC_BITS); +} + +static inline q24_8 DIV_FX(q24_8 a, q24_8 b) +{ + return ((a * (1 << FRAC_BITS)) / b); +} + +static inline q24_8 FLOOR_FX(q24_8 a) +{ + return a & (~((1 << FRAC_BITS) - 1)); +} + +// #define FMT_FX "%s%d.%03d" +// #define STR_FX(x) ((x) < 0 ? "-" : ""), ABS(FROM_FX(x)), DEC_PART(x) + +// static inline int32_t DEC_PART(q24_8 in) +// { +// return (1000 * (int32_t)(ABS(in) & ((1 << FRAC_BITS) - 1))) / (1 << FRAC_BITS); +// } +#endif + +#endif \ No newline at end of file diff --git a/main/modes/ray/mode_ray.c b/main/modes/ray/mode_ray.c new file mode 100644 index 000000000..f69cc0e72 --- /dev/null +++ b/main/modes/ray/mode_ray.c @@ -0,0 +1,174 @@ +//============================================================================== +// Includes +//============================================================================== + +#include +#include "mode_ray.h" +#include "ray_map.h" +#include "ray_renderer.h" +#include "ray_object.h" +#include "ray_tex_manager.h" +#include "ray_player.h" + +//============================================================================== +// Function Prototypes +//============================================================================== + +static void rayEnterMode(void); +static void rayExitMode(void); +static void rayMainLoop(int64_t elapsedUs); +static void rayBackgroundDrawCallback(int16_t x, int16_t y, int16_t w, int16_t h, int16_t up, int16_t upNum); + +//============================================================================== +// Const Variables +//============================================================================== + +const char rayName[] = "Magtroid Pocket"; + +//============================================================================== +// Variables +//============================================================================== + +swadgeMode_t rayMode = { + .modeName = rayName, + .wifiMode = NO_WIFI, + .overrideUsb = false, + .usesAccelerometer = false, + .usesThermometer = false, + .fnEnterMode = rayEnterMode, + .fnExitMode = rayExitMode, + .fnMainLoop = rayMainLoop, + .fnAudioCallback = NULL, + .fnBackgroundDrawCallback = rayBackgroundDrawCallback, + .fnEspNowRecvCb = NULL, + .fnEspNowSendCb = NULL, + .fnAdvancedUSB = NULL, +}; + +ray_t* ray; + +//============================================================================== +// Functions +//============================================================================== + +/** + * @brief Enter the Ray mode and initialize everything + */ +static void rayEnterMode(void) +{ + // Allocate memory + ray = calloc(1, sizeof(ray_t)); + + // Set invalid IDs for all bullets + for (uint16_t objIdx = 0; objIdx < MAX_RAY_BULLETS; objIdx++) + { + ray->bullets[objIdx].c.id = -1; + } + // lists of enemies, items, and scenery are already cleared + + loadFont("ibm_vga8.font", &ray->ibm, true); + + // Initialize texture manager and environment textures + loadEnvTextures(ray); + + // Initialize enemy templates and textures + initEnemyTemplates(ray); + + // Load the map and object data + loadRayMap("demo.rmh", ray, false); + + // Set initial position and direction, centered on the tile + initializePlayer(ray); + + // Turn off LEDs + led_t leds[CONFIG_NUM_LEDS] = {0}; + setLeds(leds, CONFIG_NUM_LEDS); +} + +/** + * @brief Exit the ray mode and free all allocated memory + */ +static void rayExitMode(void) +{ + // Empty all lists + rayEnemy_t* poppedEnemy = NULL; + while (NULL != (poppedEnemy = pop(&ray->enemies))) + { + free(poppedEnemy); + } + + rayObjCommon_t* poppedItem = NULL; + while (NULL != (poppedItem = pop(&ray->items))) + { + free(poppedItem); + } + + rayObjCommon_t* poppedScenery = NULL; + while (NULL != (poppedScenery = pop(&ray->scenery))) + { + free(poppedScenery); + } + + // Free the map + freeRayMap(&ray->map); + // Free the textures + freeAllTex(ray); + + // Free the font + freeFont(&ray->ibm); + + // Free the game state + free(ray); +} + +/** + * @brief This function is called from the main loop. It does everything + * + * @param elapsedUs The time elapsed since the last time this function was called. + */ +static void rayMainLoop(int64_t elapsedUs) +{ + // Render everything! This must be done first, to draw over the floor and ceiling, + // which were drawn in the background callback, before updating any positions or directions + // Draw the walls after floor & ceiling + castWalls(ray); + // Draw sprites after walls + rayObjCommon_t* centeredSprite = castSprites(ray); + // Draw the HUD after sprites + drawHud(ray); + + // Run timers for head-bob, doors, etc. + runEnvTimers(ray, elapsedUs); + + // Check buttons for the player and move player accordingly + rayPlayerCheckButtons(ray, centeredSprite, elapsedUs); + + // Check the joystick for the player and update loadout accordingly + rayPlayerCheckJoystick(ray, elapsedUs); + + // Check for lava damage + rayPlayerCheckLava(ray, elapsedUs); + + // Move objects including enemies and bullets + moveRayObjects(ray, elapsedUs); + + // Check for collisions between the moved player, enemies, and bullets + checkRayCollisions(ray); +} + +/** + * @brief This function is called when the display driver wishes to update a section of the background + * This is used to draw the first layer: the floor and ceiling + * + * @param x the x coordinate that should be updated + * @param y the x coordinate that should be updated + * @param w the width of the rectangle to be updated + * @param h the height of the rectangle to be updated + * @param up update number, ignored + * @param numUp update number denominator, ignored + */ +static void rayBackgroundDrawCallback(int16_t x, int16_t y, int16_t w, int16_t h, int16_t up, int16_t upNum) +{ + // Draw a portion of the background + castFloorCeiling(ray, y, y + h); +} diff --git a/main/modes/ray/mode_ray.h b/main/modes/ray/mode_ray.h new file mode 100644 index 000000000..ea9826553 --- /dev/null +++ b/main/modes/ray/mode_ray.h @@ -0,0 +1,308 @@ +#ifndef _MODE_RAY_H_ +#define _MODE_RAY_H_ + +//============================================================================== +// Includes +//============================================================================== + +#include "swadge2024.h" +#include "fp_math.h" + +//============================================================================== +// Defines +//============================================================================== + +/** The number of total maps */ +#define NUM_MAPS 6 +/** The number of missile pickups per map */ +#define MISSILE_UPGRADES_PER_MAP 3 + +/** The player's starting max health */ +#define GAME_START_HEALTH 50 +/** How much health is gained for each energy tank found */ +#define HEALTH_PER_E_TANK 25 +/** The number of energy tank pickups per map */ +#define E_TANKS_PER_MAP 1 +/** The player's total maximum possible health */ +#define MAX_HEALTH_EVER (GAME_START_HEALTH + (NUM_MAPS * E_TANKS_PER_MAP * HEALTH_PER_E_TANK)) + +/** Microseconds per one damage when standing in lava */ +#define US_PER_LAVA_DAMAGE 500000 + +/** Microseconds to charge the charge beam */ +#define CHARGE_TIME_US 2097152 + +/** The number of bullets tracked at a given point in time */ +#define MAX_RAY_BULLETS 32 + +/** The number of non-walking frames in an animation */ +#define NUM_NON_WALK_FRAMES 4 +/** The number of walking frames in an animation (non-walking doubled) */ +#define NUM_WALK_FRAMES (NUM_NON_WALK_FRAMES * 2) + +/** The time to swap out and swap in a gun, in microseconds */ +#define LOADOUT_TIMER_US (1 << 18) + +/** + * @brief Helper macro to check if a cell is of a given type + * The type is the top three bits of the type + * + * @param cell The cell to check + * @param type The type to check against (top three bits) + */ +#define CELL_IS_TYPE(cell, type) (((cell) & (0xE0)) == (type)) + +// Bits used for tile type construction, topmost bit +#define BG 0x00 +#define OBJ 0x80 +// Types of background, next two top bits +#define META 0x00 +#define FLOOR 0x20 +#define WALL 0x40 +#define DOOR 0x60 +// Types of objects, next two top bits +#define ITEM 0x00 +#define ENEMY 0x20 +#define BULLET 0x40 +#define SCENERY 0x60 + +//============================================================================== +// Enums +//============================================================================== + +/** + * @brief Tile types. The top three bits are metadata, the bottom five bits are + * a unique number per that metadata + */ +typedef enum __attribute__((packed)) +{ + // Special empty type + EMPTY = 0, // Equivalent to (BG | META | 0), + // Special delete tile, only used in map editor + DELETE = (BG | META | 1), + // Background tiles + BG_FLOOR = (BG | FLOOR | 1), + BG_FLOOR_WATER = (BG | FLOOR | 2), + BG_FLOOR_LAVA = (BG | FLOOR | 3), + BG_CEILING = (BG | FLOOR | 4), + BG_WALL_1 = (BG | WALL | 1), + BG_WALL_2 = (BG | WALL | 2), + BG_WALL_3 = (BG | WALL | 3), + BG_DOOR = (BG | DOOR | 1), + BG_DOOR_CHARGE = (BG | DOOR | 2), + BG_DOOR_MISSILE = (BG | DOOR | 3), + BG_DOOR_ICE = (BG | DOOR | 4), + BG_DOOR_XRAY = (BG | DOOR | 5), + BG_DOOR_SCRIPT = (BG | DOOR | 6), + // Enemies + OBJ_ENEMY_START_POINT = (OBJ | ENEMY | 1), + OBJ_ENEMY_NORMAL = (OBJ | ENEMY | 2), + OBJ_ENEMY_STRONG = (OBJ | ENEMY | 3), + OBJ_ENEMY_ARMORED = (OBJ | ENEMY | 4), + OBJ_ENEMY_FLAMING = (OBJ | ENEMY | 5), + OBJ_ENEMY_HIDDEN = (OBJ | ENEMY | 6), + OBJ_ENEMY_BOSS = (OBJ | ENEMY | 7), + // Power-ups + OBJ_ITEM_BEAM = (OBJ | ITEM | 1), + OBJ_ITEM_CHARGE_BEAM = (OBJ | ITEM | 2), + OBJ_ITEM_MISSILE = (OBJ | ITEM | 3), + OBJ_ITEM_ICE = (OBJ | ITEM | 4), + OBJ_ITEM_XRAY = (OBJ | ITEM | 5), + OBJ_ITEM_SUIT_WATER = (OBJ | ITEM | 6), + OBJ_ITEM_SUIT_LAVA = (OBJ | ITEM | 7), + OBJ_ITEM_ENERGY_TANK = (OBJ | ITEM | 8), + // Permanent non-power-items + OBJ_ITEM_KEY = (OBJ | ITEM | 9), + OBJ_ITEM_ARTIFACT = (OBJ | ITEM | 10), + // Transient items + OBJ_ITEM_PICKUP_ENERGY = (OBJ | ITEM | 11), + OBJ_ITEM_PICKUP_MISSILE = (OBJ | ITEM | 12), + // Bullets + OBJ_BULLET_NORMAL = (OBJ | BULLET | 13), + OBJ_BULLET_CHARGE = (OBJ | BULLET | 14), + OBJ_BULLET_ICE = (OBJ | BULLET | 15), + OBJ_BULLET_MISSILE = (OBJ | BULLET | 16), + OBJ_BULLET_XRAY = (OBJ | BULLET | 17), + // Scenery + OBJ_SCENERY_TERMINAL = (OBJ | SCENERY | 1), +} rayMapCellType_t; + +/** + * @brief Possible enemy states + */ +typedef enum +{ + E_WALKING, ///< The enemy is walking + E_SHOOTING, ///< The enemy is shooting (may move while shooting) + E_HURT, ///< The enemy was shot +} rayEnemyState_t; + +/** + * @brief All the possible loadouts + */ +typedef enum +{ + LO_NONE, ///< No loadout + LO_NORMAL, ///< Normal loadout + LO_MISSILE, ///< Missile loadout + LO_ICE, ///< Ice beam loadout + LO_XRAY, ///< X-Ray loadout + NUM_LOADOUTS ///< The number of loadouts +} rayLoadout_t; + +//============================================================================== +// Structs +//============================================================================== + +/** + * @brief A single map cell + */ +typedef struct +{ + rayMapCellType_t type; ///< The type of this cell + q24_8 doorOpen; ///< A timer for this cell, if it happens to be a door +} rayMapCell_t; + +/** + * @brief An entire map + */ +typedef struct +{ + uint32_t w; ///< The width of the map + uint32_t h; ///< The height of the map + rayMapCell_t** tiles; ///< A 2D array of tiles in the map +} rayMap_t; + +/** + * @brief A texture with a name + */ +typedef struct +{ + char* name; ///< The name of the texture + wsg_t texture; ///< An image used as a texture +} namedTexture_t; + +/** + * @brief Common data for all objects in a map + */ +typedef struct +{ + wsg_t* sprite; ///< The current sprite for this object + q24_8 posX; ///< The X position of this object + q24_8 posY; ///< The Y position of this object + q24_8 radius; ///< The radius of this object + rayMapCellType_t type; ///< The object's type + int32_t id; ///< This object's ID + bool spriteMirrored; ///< Whether or not the sprite should be drawn mirrored +} rayObjCommon_t; + +/** + * @brief Data for a bullet in the map. It has common data and velocity + */ +typedef struct +{ + rayObjCommon_t c; ///< Common object properties + q24_8 velX; ///< The X velocity of this bullet + q24_8 velY; ///< The Y velocity of this bullet +} rayBullet_t; + +/** + * @brief Data for an enemy in the map. It has common data, state tracking, and textures + */ +typedef struct +{ + rayObjCommon_t c; ///< Common object properties + rayEnemyState_t state; ///< This enemy's current state + uint32_t animTimer; ///< A timer used for this enemy's animations + uint32_t animTimerLimit; ///< The time at which the texture should switch + uint32_t animTimerFrame; ///< The current animation frame + wsg_t* walkSprites[NUM_NON_WALK_FRAMES]; ///< The walking sprites for this enemy + wsg_t* shootSprites[NUM_NON_WALK_FRAMES]; ///< The shooting sprites for this enemy + wsg_t* hurtSprites[NUM_NON_WALK_FRAMES]; ///< The getting shot sprites for this enemy +} rayEnemy_t; + +/** + * @brief The player's inventory + * TODO save to disk sometime + */ +typedef struct +{ + // Persistent pick-ups + int32_t missilesPickUps[NUM_MAPS][MISSILE_UPGRADES_PER_MAP]; ///< Coordinate list of acquired missile expansions + int32_t healthPickUps[NUM_MAPS][E_TANKS_PER_MAP]; ///< Coordinate list of acquired e.tanks + // Current status + int32_t health; ///< The player's current health + int32_t maxHealth; ///< The player's current max health. + int32_t numMissiles; ///< The player's current missile count + int32_t maxNumMissiles; ///< The player's current max missile count + // Persistent beam pickups + bool beamLoadOut; ///< True if the normal beam was acquired + bool chargePowerUp; ///< True if the charge beam was acquired + bool missileLoadOut; ///< True if a missile was acquired + bool iceLoadOut; ///< True if the ice loadout was acquired + bool xrayLoadOut; ///< True if the xray loadout was acquired + // Persistent suit pickups + bool lavaSuit; ///< True if the lava suit was acquired + bool waterSuit; ///< True if the water suit was acquired + // Key items + bool artifacts[6]; ///< List of acquired artifacts +} rayInventory_t; + +/** + * @brief The entire game state + * + */ +typedef struct +{ + rayMap_t map; ///< The loaded map + int32_t mapId; ///< The ID of the current map (TODO) + int32_t doorTimer; ///< A timer used to open doors + + rayBullet_t bullets[MAX_RAY_BULLETS]; ///< A list of all bullets + list_t enemies; ///< A list of all enemies (moves, can be shot) + list_t scenery; ///< A list of all scenery (doesn't move, can be shot) + list_t items; ///< A list of all items (doesn't move, can be shot) + + q24_8 posX; ///< The player's X position + q24_8 posY; ///< The player's Y position + q24_8 dirX; ///< The player's X direction + q24_8 dirY; ///< The player's Y direction + q24_8 planeX; ///< The X camera plane, orthogonal to dir vector + q24_8 planeY; ///< The Y camera plane, orthogonal to dir vector + + q24_8 wallDistBuffer[TFT_WIDTH]; ///< The distance of each vertical strip of pixels, used for sprite casting + + q24_8 posZ; ///< The Z position, used for head bobbing + int32_t bobTimer; ///< A timer used for head bobbing + int32_t bobCount; ///< A count used to adjust posZ sinusoidally + + uint32_t btnState; ///< The current button state + bool isStrafing; ///< true if the player is strafing, false if not + rayObjCommon_t* targetedObj; ///< An object that is locked onto to strafe around + + rayInventory_t inventory; ///< All the players items + + rayLoadout_t loadout; ///< The player's current loadout + rayLoadout_t nextLoadout; ///< The player's next loadout, if touched + int32_t loadoutChangeTimer; ///< A timer used for swapping loadouts + bool forceLoadoutSwap; ///< Force the loadout to change without touch input + + int32_t lavaTimer; ///< Timer to apply lava damage + int32_t chargeTimer; ///< Timer to charge shots + + namedTexture_t* loadedTextures; ///< A list of loaded textures + uint8_t* typeToIdxMap; ///< A map of rayMapCellType_t to respective textures + wsg_t guns[NUM_LOADOUTS]; ///< Textures for the HUD guns + + rayEnemy_t eTemplates[6]; ///< Enemy type templates, copied when initializing enemies + + font_t ibm; ///< A font to draw the HUD +} ray_t; + +//============================================================================== +// Extern variables +//============================================================================== + +extern swadgeMode_t rayMode; + +#endif diff --git a/main/modes/ray/ray_map.c b/main/modes/ray/ray_map.c new file mode 100644 index 000000000..14a80f36d --- /dev/null +++ b/main/modes/ray/ray_map.c @@ -0,0 +1,177 @@ +//============================================================================== +// Includes +//============================================================================== + +#include +#include +#include +#include + +#include +#include + +#include "hdw-spiffs.h" +#include "heatshrink_helper.h" +#include "ray_map.h" +#include "ray_tex_manager.h" +#include "ray_renderer.h" + +//============================================================================== +// Functions +//============================================================================== + +/** + * @brief Load a RMH from ROM to RAM. RMHs placed in the spiffs_image folder + * before compilation will be automatically flashed to ROM + * + * @param name The filename of the RMH to load + * @param ray The ray_t to load the map into + * @param spiRam true to load to SPI RAM, false to load to normal RAM. SPI RAM is more plentiful but slower to access + * than normal RAM + */ +void loadRayMap(const char* name, ray_t* ray, bool spiRam) +{ + // Pick the allocation type + uint32_t caps = spiRam ? MALLOC_CAP_SPIRAM : MALLOC_CAP_DEFAULT; + + // Convenience pointers + rayMap_t* map = &ray->map; + + // Read and decompress the file + uint32_t decompressedSize = 0; + uint8_t* fileData = readHeatshrinkFile(name, &decompressedSize, spiRam); + uint32_t fileIdx = 0; + + // Read the width and height + map->w = fileData[fileIdx++]; + map->h = fileData[fileIdx++]; + + // Allocate the tiles, 2D array + map->tiles = (rayMapCell_t**)heap_caps_calloc(map->w, sizeof(rayMapCell_t*), caps); + for (uint32_t x = 0; x < map->w; x++) + { + map->tiles[x] = (rayMapCell_t*)heap_caps_calloc(map->h, sizeof(rayMapCell_t), caps); + } + + // Read tile data + for (uint32_t y = 0; y < map->h; y++) + { + for (uint32_t x = 0; x < map->w; x++) + { + // Each tile has a type and object + map->tiles[x][y].type = fileData[fileIdx++]; + map->tiles[x][y].doorOpen = 0; + rayMapCellType_t type = fileData[fileIdx++]; + + // If the type isn't empty + if (EMPTY != type) + { + // Read the type's ID + uint8_t id = fileData[fileIdx++]; + // If it's the starting point + if (type == OBJ_ENEMY_START_POINT) + { + // Save the starting coordinates + ray->posX = ADD_FX(TO_FX(x), TO_FX_FRAC(1, 2)); + ray->posY = ADD_FX(TO_FX(y), TO_FX_FRAC(1, 2)); + } + // If it's an object + else if ((type & OBJ) == OBJ) + { + // Allocate a new object + if ((type & 0x60) == ENEMY) + { + // Allocate the enemy + rayEnemy_t* newObj = (rayEnemy_t*)heap_caps_calloc(1, sizeof(rayEnemy_t), MALLOC_CAP_SPIRAM); + + // Copy enemy data from the template (sprite indices, type) + memcpy(newObj, &(ray->eTemplates[type - OBJ_ENEMY_NORMAL]), sizeof(rayEnemy_t)); + + // Set ID + newObj->c.id = id; + + // Set spatial values + newObj->c.posX = TO_FX(x) + TO_FX_FRAC(1, 2); + newObj->c.posY = TO_FX(y) + TO_FX_FRAC(1, 2); + newObj->c.radius = TO_FX_FRAC(newObj->c.sprite->w, 2 * TEX_WIDTH); + + // Add it to the linked list + push(&ray->enemies, newObj); + } + else + { + // TODO check for persistent health & missile upgrades in the inventory before spawning + rayObjCommon_t* newObj + = (rayObjCommon_t*)heap_caps_calloc(1, sizeof(rayObjCommon_t), MALLOC_CAP_SPIRAM); + + // Set type, sprite and ID + newObj->type = type; + newObj->sprite = getTexByType(ray, type); + newObj->id = id; + + // Set spatial values + newObj->posX = TO_FX(x) + TO_FX_FRAC(1, 2); + newObj->posY = TO_FX(y) + TO_FX_FRAC(1, 2); + newObj->radius = TO_FX_FRAC(newObj->sprite->w, 2 * TEX_WIDTH); + + // Add it to the linked list + if ((type & 0x60) == ITEM) + { + push(&ray->items, newObj); + } + else + { + push(&ray->scenery, newObj); + } + } + } + } + } + } + + // TODO load rules!! + + // Free the file data + free(fileData); +} + +/** + * @brief Free an allocated ::rayMap_t + * + * @param map the ::rayMap_t to free + */ +void freeRayMap(rayMap_t* map) +{ + // Free each column + for (uint32_t x = 0; x < map->w; x++) + { + free(map->tiles[x]); + } + // Free the pointers + free(map->tiles); +} + +/** + * @brief Check if a cell is currently passable + * + * @param cell The cell type to check + * @return true if the cell can be passed through, false if it cannot + */ +bool isPassableCell(rayMapCell_t* cell) +{ + if (CELL_IS_TYPE(cell->type, BG | WALL)) + { + // Never pass through walls + return false; + } + else if (CELL_IS_TYPE(cell->type, BG | DOOR)) + { + // Only pass through open doors + return (TO_FX(1) == cell->doorOpen); + } + else + { + // Always pass through everything else + return true; + } +} diff --git a/main/modes/ray/ray_map.h b/main/modes/ray/ray_map.h new file mode 100644 index 000000000..acdb6c9fd --- /dev/null +++ b/main/modes/ray/ray_map.h @@ -0,0 +1,10 @@ +#ifndef _RAY_MAP_H_ +#define _RAY_MAP_H_ + +#include "mode_ray.h" + +void loadRayMap(const char* name, ray_t* ray, bool spiRam); +void freeRayMap(rayMap_t* map); +bool isPassableCell(rayMapCell_t* cell); + +#endif \ No newline at end of file diff --git a/main/modes/ray/ray_object.c b/main/modes/ray/ray_object.c new file mode 100644 index 000000000..c16411511 --- /dev/null +++ b/main/modes/ray/ray_object.c @@ -0,0 +1,464 @@ +//============================================================================== +// Includes +//============================================================================== + +#include "ray_object.h" +#include "ray_tex_manager.h" +#include "ray_renderer.h" +#include "ray_map.h" +#include "ray_player.h" + +//============================================================================== +// Function Prototypes +//============================================================================== + +static bool objectsIntersect(const rayObjCommon_t* obj1, const rayObjCommon_t* obj2); +static void moveRayBullets(ray_t* ray, int32_t elapsedUs); +static void moveRayEnemies(ray_t* ray, int32_t elapsedUs); +static void moveEnemyRook(ray_t* ray, rayEnemy_t* enemy, q24_8 pPosX, q24_8 pPosY, int32_t elapsedUs); +static void animateEnemy(rayEnemy_t* enemy, uint32_t elapsedUs); + +//============================================================================== +// Functions +//============================================================================== + +/** + * @brief Initialize templates for enemy creation. This also loads enemy sprites + * + * @param ray The entire game state + */ +void initEnemyTemplates(ray_t* ray) +{ + // The names of the different types of enemies + const char* eTypes[] = { + "NORMAL", "STRONG", "ARMORED", "FLAMING", "HIDDEN", "BOSS", + }; + + // The types of enemies + const rayMapCellType_t types[] = { + OBJ_ENEMY_NORMAL, OBJ_ENEMY_STRONG, OBJ_ENEMY_ARMORED, OBJ_ENEMY_FLAMING, OBJ_ENEMY_HIDDEN, OBJ_ENEMY_BOSS, + }; + + // An empty buffer to build strings + char buf[64] = {0}; + + // For each enemy type + for (int32_t eIdx = 0; eIdx < ARRAY_SIZE(ray->eTemplates); eIdx++) + { + // Set the type + ray->eTemplates[eIdx].c.type = types[eIdx]; + + // Set the time for each animation state + ray->eTemplates[eIdx].animTimerLimit = 250000; + + // Load textures for all states + for (int32_t frIdx = 0; frIdx < ARRAY_SIZE(ray->eTemplates->hurtSprites); frIdx++) + { + // Load the textures + snprintf(buf, sizeof(buf) - 1, "E_%s_WALK_%" PRId32 ".wsg", eTypes[eIdx], frIdx); + ray->eTemplates[eIdx].walkSprites[frIdx] = loadTexture(ray, buf, EMPTY); + snprintf(buf, sizeof(buf) - 1, "E_%s_SHOOT_%" PRId32 ".wsg", eTypes[eIdx], frIdx); + ray->eTemplates[eIdx].shootSprites[frIdx] = loadTexture(ray, buf, EMPTY); + snprintf(buf, sizeof(buf) - 1, "E_%s_HURT_%" PRId32 ".wsg", eTypes[eIdx], frIdx); + ray->eTemplates[eIdx].hurtSprites[frIdx] = loadTexture(ray, buf, EMPTY); + } + // Set initial texture + ray->eTemplates[eIdx].c.sprite = ray->eTemplates[eIdx].walkSprites[0]; + } +} + +/** + * @brief Create a bullet with an owner, type, position, and velocity + * + * @param ray The entire game state + * @param bulletType The type of bullet + * @param posX The X position of the spawner. Bullet will be positioned slightly in front of the given position + * @param posY The X position of the spawner. Bullet will be positioned slightly in front of the given position + * @param velX The X velocity of the bullet + * @param velY The Y velocity of the bullet + * @param isPlayer true if this is the player's shot, false if it is an enemy's shot + */ +void rayCreateBullet(ray_t* ray, rayMapCellType_t bulletType, q24_8 posX, q24_8 posY, q24_8 velX, q24_8 velY, + bool isPlayer) +{ + // Iterate over the bullet list, finding a new slot + for (uint32_t newIdx = 0; newIdx < MAX_RAY_BULLETS; newIdx++) + { + // If this slot has a negative ID, use it + if (-1 == ray->bullets[newIdx].c.id) + { + // Get a convenience pointer + rayBullet_t* newBullet = &ray->bullets[newIdx]; + + // Initialize the bullet + newBullet->c.type = bulletType; + // Bullets IDs are just if it's owned by the player or not + newBullet->c.id = isPlayer ? 1 : 0; + + // Set the texture + wsg_t* texture = getTexByType(ray, bulletType); + newBullet->c.sprite = texture; + // Width is based on the texture width as a fraction of a cell + newBullet->c.radius = TO_FX_FRAC(texture->w, 2 * TEX_WIDTH); + + // Spawn it slightly in front of the shooter's position + newBullet->c.posX = posX + (velX / 4); + newBullet->c.posY = posY + (velY / 4); + + // Set the velocity + newBullet->velX = velX; + newBullet->velY = velY; + + // All done + return; + } + } +} + +/** + * @brief Move all bullets and enemies + * + * @param ray The entire game state + * @param elapsedUs The elapsed time since this function was last called + */ +void moveRayObjects(ray_t* ray, int32_t elapsedUs) +{ + moveRayBullets(ray, elapsedUs); + moveRayEnemies(ray, elapsedUs); +} + +/** + * @brief Move all bullets and check for collisions with doors + * + * @param ray The entire game state + * @param elapsedUs The elapsed time since this function was last called + */ +static void moveRayBullets(ray_t* ray, int32_t elapsedUs) +{ + // For each bullet slot + for (uint32_t i = 0; i < MAX_RAY_BULLETS; i++) + { + // If a bullet is in the slot + rayBullet_t* obj = &(ray->bullets[i]); + if (-1 != obj->c.id) + { + // Update the bullet's position + // TODO justify the scaling factor, assuming velXY is a unit vector + obj->c.posX += (obj->velX * elapsedUs) / 100000; + obj->c.posY += (obj->velY * elapsedUs) / 100000; + + // Get the cell the bullet is in now + rayMapCell_t* cell = &ray->map.tiles[FROM_FX(obj->c.posX)][FROM_FX(obj->c.posY)]; + + // If a wall is it + if (CELL_IS_TYPE(cell->type, BG | WALL)) + { + // Destroy this bullet + memset(obj, 0, sizeof(rayBullet_t)); + obj->c.id = -1; + } + // Else if a door is hit + else if (CELL_IS_TYPE(cell->type, BG | DOOR)) + { + // Check if the bullet type can open the door + if ((BG_DOOR == cell->type) // Normal doors are openable by anything + || (BG_DOOR_CHARGE == cell->type && OBJ_BULLET_CHARGE == obj->c.type) + || (BG_DOOR_SCRIPT == cell->type) // TODO disable shooting script doors + || (BG_DOOR_MISSILE == cell->type && OBJ_BULLET_MISSILE == obj->c.type) + || (BG_DOOR_ICE == cell->type && OBJ_BULLET_ICE == obj->c.type) + || (BG_DOOR_XRAY == cell->type && OBJ_BULLET_XRAY == obj->c.type)) + { + // If the door is closed + if (0 == cell->doorOpen) + { + // Start opening the door + cell->doorOpen = 1; + // Destroy this bullet + memset(obj, 0, sizeof(rayBullet_t)); + obj->c.id = -1; + } + } + } + } + } +} + +/** + * @brief Move all enemies + * + * @param ray The entire game state + * @param elapsedUs The elapsed time since this function was last called + */ +static void moveRayEnemies(ray_t* ray, int32_t elapsedUs) +{ + // Iterate over the linked list + node_t* currentNode = ray->enemies.first; + while (currentNode != NULL) + { + // Get a pointer from the linked list + rayEnemy_t* obj = ((rayEnemy_t*)currentNode->val); + + // Move enemies + moveEnemyRook(ray, obj, ray->posX, ray->posY, elapsedUs); + // Also animate enemies as they move + animateEnemy(obj, elapsedUs); + + // Iterate to the next node + currentNode = currentNode->next; + } +} + +/** + * @brief Simple movement function which has the enemy walk towards the player on the X and Y axes only + * + * @param ray The entire game state + * @param enemy The enemy to move + * @param pPosX The X position of the player + * @param pPosY The Y position of the player + * @param elapsedUs The elapsed time since this function was last called + */ +static void moveEnemyRook(ray_t* ray, rayEnemy_t* enemy, q24_8 pPosX, q24_8 pPosY, int32_t elapsedUs) +{ + q24_8 delX = SUB_FX(pPosX, enemy->c.posX); // positive if the player is to the right + q24_8 delY = SUB_FX(pPosY, enemy->c.posY); // positive if the player is above + + q24_8 sqrDist = ADD_FX(MUL_FX(delX, delX), MUL_FX(delY, delY)); + if (sqrDist > TO_FX(2)) + { + if (ABS(delX) > ABS(delY)) + { + q24_8 mDelX = 0; + if (delX > 0) + { + // Move rightward + // TODO scale with elapsedUs + mDelX = TO_FX_FRAC(1, 12); + } + else + { + // Move leftward + // TODO scale with elapsedUs + mDelX = -TO_FX_FRAC(1, 12); + } + + // Bounds check + if (isPassableCell(&ray->map.tiles[FROM_FX(enemy->c.posX + mDelX)][FROM_FX(enemy->c.posY)])) + { + enemy->c.posX += mDelX; + } + } + else + { + q24_8 mDelY = 0; + if (delY > 0) + { + // Move up + // TODO scale with elapsedUs + mDelY = TO_FX_FRAC(1, 12); + } + else + { + // Move down + // TODO scale with elapsedUs + mDelY = -TO_FX_FRAC(1, 12); + } + + // Bounds check + if (isPassableCell(&ray->map.tiles[FROM_FX(enemy->c.posX)][FROM_FX(enemy->c.posY + mDelY)])) + { + enemy->c.posY += mDelY; + } + } + } +} + +/** + * @brief Animate a single enemy + * + * @param enemy The enemy to animate + * @param elapsedUs The elapsed time since this function was last called + */ +static void animateEnemy(rayEnemy_t* enemy, uint32_t elapsedUs) +{ + // Accumulate time + enemy->animTimer += elapsedUs; + // Check if it's time to transition states + if (enemy->animTimer >= enemy->animTimerLimit) + { + // Decrement timer + enemy->animTimer -= enemy->animTimerLimit; + + // Move to next frame + if (E_WALKING == enemy->state) + { + // TODO decide when to transition to shooting + + // Walking has double the number of frames and cycles + enemy->animTimerFrame = (enemy->animTimerFrame + 1) % NUM_WALK_FRAMES; + + // Pick the sprite accordingly, and mirror the back half + enemy->c.sprite = enemy->walkSprites[enemy->animTimerFrame % NUM_NON_WALK_FRAMES]; + enemy->c.spriteMirrored = (enemy->animTimerFrame >= NUM_NON_WALK_FRAMES); + } + else + { + // Move to the next frame + enemy->animTimerFrame++; + + // If the sequence is over + if (enemy->animTimerFrame >= NUM_NON_WALK_FRAMES) + { + // Return to walking + enemy->state = E_WALKING; + enemy->animTimerFrame = 0; + enemy->c.sprite = enemy->walkSprites[0]; + } + else if (E_SHOOTING == enemy->state) + { + // Pick the next shooting sprite + enemy->c.sprite = enemy->shootSprites[enemy->animTimerFrame]; + // TODO spawn a bullet on the Nth frame + } + else // Must be E_HURT + { + // Pick the next hurt sprite + enemy->c.sprite = enemy->hurtSprites[enemy->animTimerFrame]; + } + } + } +} + +/** + * @brief Check if two ::rayObjCommon_t intersect + * + * @param obj1 The first rayObjCommon_t to check for intersection + * @param obj2 The second rayObjCommon_t to check for intersection + * @return true if they intersect, false if they do not + */ +static bool objectsIntersect(const rayObjCommon_t* obj1, const rayObjCommon_t* obj2) +{ + q24_8 deltaX = (obj2->posX - obj1->posX); + q24_8 deltaY = (obj2->posY - obj1->posY); + q24_8 radiusSum = (obj1->radius + obj2->radius); + return (deltaX * deltaX) + (deltaY * deltaY) < (radiusSum * radiusSum); +} + +/** + * @brief Check for collisions between bullets, enemies, and the player + * + * @param ray The entire game state + */ +void checkRayCollisions(ray_t* ray) +{ + // Create a 'player' for collision comparison + rayObjCommon_t player = { + .posX = ray->posX, + .posY = ray->posY, + .radius = TO_FX_FRAC(32, 2 * TEX_WIDTH), + }; + + // Check if a bullet touches a player + for (uint16_t bIdx = 0; bIdx < MAX_RAY_BULLETS; bIdx++) + { + rayBullet_t* bullet = &ray->bullets[bIdx]; + if (0 == bullet->c.id) + { + // An enemy's bullet + if (objectsIntersect(&player, &bullet->c)) + { + // TODO Player got shot, apply damage + // De-allocate the bullet + bullet->c.id = -1; + } + } + } + + // Check if the player touches an item + node_t* currentNode = ray->items.first; + while (currentNode != NULL) + { + // Get a pointer from the linked list + rayObjCommon_t* item = ((rayObjCommon_t*)currentNode->val); + + node_t* toRemove = NULL; + // Check intersection + if (objectsIntersect(&player, item)) + { + // Touch the item + rayPlayerTouchItem(ray, item->type, ray->mapId, item->id); + toRemove = currentNode; + } + + // Iterate to the next node + currentNode = currentNode->next; + + // If the prior node should be removed + if (toRemove) + { + // Remove the lock + if (ray->targetedObj == item) + { + ray->targetedObj = NULL; + } + // Free the item + free(item); + // Remove it from the list + removeEntry(&ray->items, toRemove); + } + } + + // Check if a bullet touches an enemy + currentNode = ray->enemies.first; + while (currentNode != NULL) + { + // Get a pointer from the linked list + rayEnemy_t* enemy = ((rayEnemy_t*)currentNode->val); + + // Iterate through all bullets + for (uint16_t bIdx = 0; bIdx < MAX_RAY_BULLETS; bIdx++) + { + rayBullet_t* bullet = &ray->bullets[bIdx]; + if (1 == bullet->c.id) + { + // A player's bullet + if (objectsIntersect(&enemy->c, &bullet->c)) + { + // TODO enemy got shot, apply damage + // De-allocate the bullet + bullet->c.id = -1; + } + } + } + + // Iterate to the next node + currentNode = currentNode->next; + } + + // Check if a bullet touches scenery + currentNode = ray->scenery.first; + while (currentNode != NULL) + { + // Get a pointer from the linked list + rayObjCommon_t* scenery = ((rayObjCommon_t*)currentNode->val); + + // Iterate through all bullets + for (uint16_t bIdx = 0; bIdx < MAX_RAY_BULLETS; bIdx++) + { + rayBullet_t* bullet = &ray->bullets[bIdx]; + if (1 == bullet->c.id) + { + // A player's bullet + if (objectsIntersect(scenery, &bullet->c)) + { + // Scenery was shot + printf("SHOT SCENERY %d\n", scenery->type); + // De-allocate the bullet + bullet->c.id = -1; + } + } + } + + // Iterate to the next node + currentNode = currentNode->next; + } +} diff --git a/main/modes/ray/ray_object.h b/main/modes/ray/ray_object.h new file mode 100644 index 000000000..eabce0d71 --- /dev/null +++ b/main/modes/ray/ray_object.h @@ -0,0 +1,12 @@ +#ifndef _RAY_OBJECT_H_ +#define _RAY_OBJECT_H_ + +#include "mode_ray.h" + +void initEnemyTemplates(ray_t* ray); +void rayCreateBullet(ray_t* ray, rayMapCellType_t bulletType, q24_8 posX, q24_8 posY, q24_8 velX, q24_8 velY, + bool isPlayer); +void moveRayObjects(ray_t* ray, int32_t elapsedUs); +void checkRayCollisions(ray_t* ray); + +#endif diff --git a/main/modes/ray/ray_player.c b/main/modes/ray/ray_player.c new file mode 100644 index 000000000..8423e1d2c --- /dev/null +++ b/main/modes/ray/ray_player.c @@ -0,0 +1,528 @@ +//============================================================================== +// Includes +//============================================================================== + +#include "hdw-btn.h" +#include "touchUtils.h" + +#include "ray_player.h" +#include "ray_object.h" +#include "ray_map.h" + +//============================================================================== +// Functions +//============================================================================== + +/** + * @brief Initialize the player + * + * @param ray The entire game state + */ +void initializePlayer(ray_t* ray) +{ + // ray->posX and ray->posY (position) are set by loadRayMap() + // Set the direction + ray->dirX = TO_FX(0); + ray->dirY = -TO_FX(1); + // the 2d rayCaster version of camera plane, orthogonal to the direction vector and scaled to 2/3 + ray->planeX = -MUL_FX(TO_FX(2) / 3, ray->dirY); + ray->planeY = MUL_FX(TO_FX(2) / 3, ray->dirX); + // Set the head-bob to centered + ray->posZ = TO_FX(0); + + // Empty out the inventory + // TODO only do this on first boot-up + memset(&ray->inventory, 0, sizeof(ray->inventory)); + for (int32_t mapIdx = 0; mapIdx < NUM_MAPS; mapIdx++) + { + for (int32_t pIdx = 0; pIdx < MISSILE_UPGRADES_PER_MAP; pIdx++) + { + ray->inventory.missilesPickUps[mapIdx][pIdx] = -1; + } + for (int32_t pIdx = 0; pIdx < E_TANKS_PER_MAP; pIdx++) + { + ray->inventory.healthPickUps[mapIdx][pIdx] = -1; + } + } + // Set initial health + ray->inventory.maxHealth = GAME_START_HEALTH; + ray->inventory.health = GAME_START_HEALTH; +} + +/** + * @brief Check button inputs for the player. This will move the player and shoot bullets + * + * @param ray The entire game state + * @param centeredSprite The sprite currently centered in the view + * @param elapsedUs The elapsed time since this function was last called + */ +void rayPlayerCheckButtons(ray_t* ray, rayObjCommon_t* centeredSprite, uint32_t elapsedUs) +{ + // Check all queued button events + uint32_t prevBtnState = ray->btnState; + buttonEvt_t evt; + while (checkButtonQueueWrapper(&evt)) + { + ray->btnState = evt.state; + } + + // If the player is in water without the water suit + bool isInWater = (!ray->inventory.waterSuit) + && (BG_FLOOR_WATER == ray->map.tiles[FROM_FX(ray->posX)][FROM_FX(ray->posY)].type); + + // Find move distances + q24_8 deltaX = 0; + q24_8 deltaY = 0; + + // B button strafes, which may lock on an enemy + if ((ray->btnState & PB_B) && !(prevBtnState & PB_B)) + { + // Set strafe to true + ray->isStrafing = true; + // If there is a centered sprite + if (centeredSprite) + { + // Lock onto it + ray->targetedObj = centeredSprite; + } + } + else if (!(ray->btnState & PB_B) && (prevBtnState & PB_B)) + { + // Set strafe to false + ray->isStrafing = false; + ray->targetedObj = NULL; + } + + // Strafing is either locked or unlocked + if (ray->isStrafing) + { + if (ray->btnState & PB_RIGHT) + { + // Strafe right + // TODO scale with elapsed time + deltaX -= (ray->dirY / 6); + deltaY += (ray->dirX / 6); + } + else if (ray->btnState & PB_LEFT) + { + // Strafe left + // TODO scale with elapsed time + deltaX += (ray->dirY / 6); + deltaY -= (ray->dirX / 6); + } + } + else + { + // Assume no rotation + int32_t rotateDeg = 0; + + if (ray->btnState & PB_RIGHT) + { + // Rotate right + // TODO scale with elapsed time + rotateDeg = 5; + } + else if (ray->btnState & PB_LEFT) + { + // Rotate left + // TODO scale with elapsed time + rotateDeg = 355; + } + + // If we should rotate + if (rotateDeg) + { + // Do trig functions, only once + int32_t sinVal = getSin1024(rotateDeg); + int32_t cosVal = getCos1024(rotateDeg); + // Find the rotated X and Y vectors + q24_8 newX = (ray->dirX * cosVal) - (ray->dirY * sinVal); + q24_8 newY = (ray->dirX * sinVal) + (ray->dirY * cosVal); + ray->dirX = newX; + ray->dirY = newY; + // Normalize the vector + fastNormVec(&ray->dirX, &ray->dirY); + + // Recompute the camera plane, orthogonal to the direction vector and scaled to 2/3 + ray->planeX = -MUL_FX(TO_FX(2) / 3, ray->dirY); + ray->planeY = MUL_FX(TO_FX(2) / 3, ray->dirX); + } + } + + // If the up button is held + if (ray->btnState & PB_UP) + { + // Move forward + // TODO scale with elapsed time + deltaX += (ray->dirX / 6); + deltaY += (ray->dirY / 6); + } + // Else if the down button is held + else if (ray->btnState & PB_DOWN) + { + // Move backwards + // TODO scale with elapsed time + deltaX -= (ray->dirX / 6); + deltaY -= (ray->dirY / 6); + } + + // TODO normalize deltaX and deltaY to something scaled with elapsed time + + // If the player is in water + if (isInWater) + { + // Slow down movement by a fourth + deltaX /= 4; + deltaY /= 4; + } + + // Boundary checks are longer than the move dist to not get right up on the wall + q24_8 boundaryCheckX = deltaX * 2; + q24_8 boundaryCheckY = deltaY * 2; + + // Move forwards if no wall in front of you + if (isPassableCell(&ray->map.tiles[FROM_FX(ray->posX + boundaryCheckX)][FROM_FX(ray->posY)])) + { + ray->posX += deltaX; + } + + if (isPassableCell(&ray->map.tiles[FROM_FX(ray->posX)][FROM_FX(ray->posY + boundaryCheckY)])) + { + ray->posY += deltaY; + } + + // After moving position, recompute direction to targeted object + if (ray->isStrafing && ray->targetedObj) + { + // Re-lock on the target after moving + ray->dirX = ray->targetedObj->posX - ray->posX; + ray->dirY = ray->targetedObj->posY - ray->posY; + fastNormVec(&ray->dirX, &ray->dirY); + + // Recompute the 2d rayCaster version of camera plane, orthogonal to the direction vector and scaled to 2/3 + ray->planeX = -MUL_FX(TO_FX(2) / 3, ray->dirY); + ray->planeY = MUL_FX(TO_FX(2) / 3, ray->dirX); + } + + // If the player has a gun + if (LO_NONE != ray->loadout) + { + // What, if any, bullet to fire + rayMapCellType_t bullet = EMPTY; + + // If A was pressed + if ((ray->btnState & PB_A) && !(prevBtnState & PB_A)) + { + // Check ammo for the missile loadout + if (LO_MISSILE == ray->loadout) + { + if (0 < ray->inventory.numMissiles) + { + // Decrement missile count + ray->inventory.numMissiles--; + // Fire a missile + bullet = OBJ_BULLET_MISSILE; + } + } + else + { + // Start charging if applicable + if (LO_NORMAL == ray->loadout && ray->inventory.chargePowerUp) + { + // Start charging + ray->chargeTimer = 1; + } + + // Fire according to loadout + const rayMapCellType_t bulletMap[NUM_LOADOUTS] = { + EMPTY, + OBJ_BULLET_NORMAL, ///< Normal loadout + OBJ_BULLET_MISSILE, ///< Missile loadout + OBJ_BULLET_ICE, ///< Ice beam loadout + OBJ_BULLET_XRAY ///< X-Ray loadout + }; + bullet = bulletMap[ray->loadout]; + } + } + else if (!(ray->btnState & PB_A) && (prevBtnState & PB_A)) + { + // A was released, check if the beam is charged + if (ray->chargeTimer >= CHARGE_TIME_US) + { + // Shoot charge beam + bullet = OBJ_BULLET_CHARGE; + } + ray->chargeTimer = 0; + } + + // If charging + if (0 < ray->chargeTimer && ray->chargeTimer <= CHARGE_TIME_US) + { + // Accumulate the timer + ray->chargeTimer += elapsedUs; + } + + // If there is a bullet to fire + if (EMPTY != bullet) + { + // Fire a shot + rayCreateBullet(ray, bullet, ray->posX, ray->posY, ray->dirX, ray->dirY, true); + } + } +} + +/** + * @brief Check touchpad inputs for the player. This will change the player's loadout + * + * @param ray The entire game state + * @param elapsedUs The elapsed time since this function was last called + */ +void rayPlayerCheckJoystick(ray_t* ray, uint32_t elapsedUs) +{ + // Check if the touch area is touched, and print values if it is + int32_t phi, r, intensity; + if (getTouchJoystick(&phi, &r, &intensity)) + { + // If there isn't a loadout change in progress + if (ray->loadoutChangeTimer == 0) + { + // Get the zones from the touchpad + touchJoystick_t tj = getTouchJoystickZones(phi, r, true, false); + if (!(tj & TB_CENTER)) + { + // Get the loadout touched + rayLoadout_t nextLoadout = ray->loadout; + if ((tj & TB_UP) && (ray->inventory.beamLoadOut)) + { + nextLoadout = LO_NORMAL; + } + else if ((tj & TB_RIGHT) && (ray->inventory.missileLoadOut)) + { + nextLoadout = LO_MISSILE; + } + else if ((tj & TB_LEFT) && (ray->inventory.iceLoadOut)) + { + nextLoadout = LO_ICE; + } + else if ((tj & TB_DOWN) && (ray->inventory.xrayLoadOut)) + { + nextLoadout = LO_XRAY; + } + + // If a new loadout was touched + if (ray->loadout != nextLoadout) + { + // Start a timer to switch to the next loadout + ray->loadoutChangeTimer = LOADOUT_TIMER_US; + ray->nextLoadout = nextLoadout; + } + } + } + } + else + { + // Button Not touched. If this was during a loadout change, cancel it + if ((ray->nextLoadout != ray->loadout) && !ray->forceLoadoutSwap) + { + // Reset the timer to bring the gun up and set the next loadout to the current one + ray->loadoutChangeTimer = LOADOUT_TIMER_US - ray->loadoutChangeTimer; + ray->nextLoadout = ray->loadout; + } + } + + // If a loadout change is in progress + if (ray->loadoutChangeTimer) + { + // Decrement the timer + ray->loadoutChangeTimer -= elapsedUs; + + // If the timer elapsed + if (ray->loadoutChangeTimer <= 0) + { + if (ray->loadout != ray->nextLoadout) + { + // Swap the loadout + ray->loadout = ray->nextLoadout; + // Set the timer for the load in + ray->loadoutChangeTimer = LOADOUT_TIMER_US; + } + else + { + // All done swapping + ray->loadoutChangeTimer = 0; + ray->forceLoadoutSwap = false; + } + } + } +} + +/** + * @brief This handles what happens when a player touches an item + * + * @param ray The whole game state + * @param type The type of item touched + * @param mapId The current map ID, used to track non-unique persistent pick-ups + * @param itemId The item's ID + */ +void rayPlayerTouchItem(ray_t* ray, rayMapCellType_t type, int32_t mapId, int32_t itemId) +{ + rayInventory_t* inventory = &ray->inventory; + switch (type) + { + case OBJ_ITEM_BEAM: + { + inventory->beamLoadOut = true; + // Switch to the normal loadout + ray->loadoutChangeTimer = LOADOUT_TIMER_US; + ray->nextLoadout = LO_NORMAL; + ray->forceLoadoutSwap = true; + break; + } + case OBJ_ITEM_CHARGE_BEAM: + { + inventory->chargePowerUp = true; + if (LO_NORMAL != ray->loadout) + { + // Switch to the normal loadout + ray->loadoutChangeTimer = LOADOUT_TIMER_US; + ray->nextLoadout = LO_NORMAL; + ray->forceLoadoutSwap = true; + } + break; + } + case OBJ_ITEM_ICE: + { + inventory->iceLoadOut = true; + // Switch to the ice loadout + ray->loadoutChangeTimer = LOADOUT_TIMER_US; + ray->nextLoadout = LO_ICE; + ray->forceLoadoutSwap = true; + break; + } + case OBJ_ITEM_XRAY: + { + inventory->xrayLoadOut = true; + // Switch to the xray loadout + ray->loadoutChangeTimer = LOADOUT_TIMER_US; + ray->nextLoadout = LO_XRAY; + ray->forceLoadoutSwap = true; + break; + } + case OBJ_ITEM_SUIT_WATER: + { + inventory->waterSuit = true; + break; + } + case OBJ_ITEM_SUIT_LAVA: + { + inventory->lavaSuit = true; + break; + } + case OBJ_ITEM_MISSILE: + { + // Find a slot to save this missile pickup + for (int32_t idx = 0; idx < MISSILE_UPGRADES_PER_MAP; idx++) + { + // The ID of an item can't be -1, so this is free + if (-1 == inventory->missilesPickUps[mapId][idx]) + { + if (!inventory->missileLoadOut) + { + // Picking up any missiles enables the loadout + inventory->missileLoadOut = true; + // Switch to the missile loadout + ray->loadoutChangeTimer = LOADOUT_TIMER_US; + ray->nextLoadout = LO_MISSILE; + ray->forceLoadoutSwap = true; + } + // Add five missiles + inventory->numMissiles += 5; + inventory->maxNumMissiles += 5; + + // Save the coordinates + inventory->missilesPickUps[mapId][idx] = itemId; + break; + } + } + break; + } + case OBJ_ITEM_ENERGY_TANK: + { + // Find a slot to save this health pickup + for (int32_t idx = 0; idx < E_TANKS_PER_MAP; idx++) + { + // The ID of an item can't be -1, so this is free + if (-1 == inventory->healthPickUps[mapId][idx]) + { + // Add max health + inventory->maxHealth += HEALTH_PER_E_TANK; + // Reset health + inventory->health = inventory->maxHealth; + + // Save the coordinates + inventory->healthPickUps[mapId][idx] = itemId; + break; + } + } + break; + } + case OBJ_ITEM_ARTIFACT: + { + inventory->artifacts[mapId] = true; + break; + } + case OBJ_ITEM_PICKUP_ENERGY: + { + // Transient, add 20 health, not going over the max + inventory->health = MIN(inventory->health + 20, inventory->maxHealth); + break; + } + case OBJ_ITEM_PICKUP_MISSILE: + { + if (ray->inventory.missileLoadOut) + { + // Transient, add 5 missiles, not going over the max + inventory->numMissiles = MIN(inventory->numMissiles + 5, inventory->maxNumMissiles); + } + break; + } + case OBJ_ITEM_KEY: + { + // TODO implement keys? + break; + } + default: + { + // Don't care about other types + break; + } + } +} + +/** + * @brief Check if a player should take damage for standing in lava + * + * @param ray The entire game state + * @param elapsedUs The elapsed time since this function was last called + */ +void rayPlayerCheckLava(ray_t* ray, uint32_t elapsedUs) +{ + // If the player is in lava without the lava suit + if ((!ray->inventory.lavaSuit) && (BG_FLOOR_LAVA == ray->map.tiles[FROM_FX(ray->posX)][FROM_FX(ray->posY)].type)) + { + // Run a timer to take lava damage + ray->lavaTimer += elapsedUs; + if (ray->lavaTimer <= US_PER_LAVA_DAMAGE) + { + ray->lavaTimer -= US_PER_LAVA_DAMAGE; + if (ray->inventory.health) + { + ray->inventory.health--; + if (0 == ray->inventory.health) + { + // TODO game over + } + } + } + } +} diff --git a/main/modes/ray/ray_player.h b/main/modes/ray/ray_player.h new file mode 100644 index 000000000..23c169604 --- /dev/null +++ b/main/modes/ray/ray_player.h @@ -0,0 +1,12 @@ +#ifndef _RAY_PLAYER_H_ +#define _RAY_PLAYER_H_ + +#include "mode_ray.h" + +void initializePlayer(ray_t* ray); +void rayPlayerCheckButtons(ray_t* ray, rayObjCommon_t* centeredSprite, uint32_t elapsedUs); +void rayPlayerCheckJoystick(ray_t* ray, uint32_t elapsesUs); +void rayPlayerTouchItem(ray_t* ray, rayMapCellType_t type, int32_t mapId, int32_t itemId); +void rayPlayerCheckLava(ray_t* ray, uint32_t elapsedUs); + +#endif \ No newline at end of file diff --git a/main/modes/ray/ray_renderer.c b/main/modes/ray/ray_renderer.c new file mode 100644 index 000000000..56f3c1188 --- /dev/null +++ b/main/modes/ray/ray_renderer.c @@ -0,0 +1,1057 @@ +//============================================================================== +// Includes +//============================================================================== + +#include "mode_ray.h" +#include "ray_renderer.h" +#include "ray_tex_manager.h" +#include "fp_math.h" +#include "hdw-tft.h" + +//============================================================================== +// Defines +//============================================================================== + +// When casting floor and ceiling, use 8 more decimal bits and 8 fewer whole number bits +#define EX_CEIL_PRECISION_BITS 8 + +// Half width of the lock zone when locking on enemies +#define LOCK_ZONE 16 + +//============================================================================== +// Structs +//============================================================================== + +/// Helper struct to sort rayObjCommon_t by distance from the player +typedef struct +{ + rayObjCommon_t* obj; + uint32_t dist; +} objDist_t; + +//============================================================================== +// Const data +//============================================================================== + +// LUT for a palette swap when in X-Ray loadout mode +const paletteColor_t xrayPaletteSwap[] + = {c555, c554, c553, c552, c551, c550, c545, c544, c543, c542, c541, c540, c535, c534, c533, c532, c531, + c530, c525, c524, c523, c522, c521, c520, c515, c514, c513, c512, c511, c510, c505, c504, c503, c502, + c501, c500, c455, c454, c453, c452, c451, c450, c445, c444, c443, c442, c441, c440, c435, c434, c433, + c432, c431, c430, c425, c424, c423, c422, c421, c420, c415, c414, c413, c412, c411, c410, c405, c404, + c403, c402, c401, c400, c355, c354, c353, c352, c351, c350, c345, c344, c343, c342, c341, c340, c335, + c334, c333, c332, c331, c330, c325, c324, c323, c322, c321, c320, c315, c314, c313, c312, c311, c310, + c305, c304, c303, c302, c301, c300, c255, c254, c253, c252, c251, c250, c245, c244, c243, c242, c241, + c240, c235, c234, c233, c232, c231, c230, c225, c224, c223, c222, c221, c220, c215, c214, c213, c212, + c211, c210, c205, c204, c203, c202, c201, c200, c155, c154, c153, c152, c151, c150, c145, c144, c143, + c142, c141, c140, c135, c134, c133, c132, c131, c130, c125, c124, c123, c122, c121, c120, c115, c114, + c113, c112, c111, c110, c105, c104, c103, c102, c101, c100, c055, c054, c053, c052, c051, c050, c045, + c044, c043, c042, c041, c040, c035, c034, c033, c032, c031, c030, c025, c024, c023, c022, c021, c020, + c015, c014, c013, c012, c011, c010, c005, c004, c003, c002, c001, c000, cTransparent}; + +//============================================================================== +// Function Prototypes +//============================================================================== + +static int objDistComparator(const void* obj1, const void* obj2); +static bool rayIntersectsDoor(bool side, int32_t mapX, int32_t mapY, q24_8 posX, q24_8 posY, q24_8 rayDirX, + q24_8 rayDirY, q24_8 deltaDistX, q24_8 deltaDistY, q24_8 doorOpen); + +//============================================================================== +// Functions +//============================================================================== + +/** + * @brief Draw a section of the floor and ceiling pixels. The floor and ceiling are drawn first. This iterates over the + * screen top-to-bottom and draws horizontal rows. + * This is called from the background draw callback + * + * @param ray The entire game state + * @param firstRow The first row to draw + * @param lastRow The last row to draw + */ +void castFloorCeiling(ray_t* ray, int32_t firstRow, int32_t lastRow) +{ + // We'll be drawing pixels, so set this up + SETUP_FOR_TURBO(); + + // Boolean if the colors should be drawn inverted + bool isXray = (LO_XRAY == ray->loadout); + + // Track which cell the ceiling or floor is being drawn in + uint32_t cellX = 0; + uint32_t cellY = 0; + + // Track the last cell the ceiling or floor was drawn in to reset textures which it changes + uint32_t lastCellX = -1; + uint32_t lastCellY = -1; + + // Set up variables for fixed point texture coordinates + q16_16 texPosX = 0; + q16_16 texPosY = 0; + + // Set up variables for integer texture indices + uint32_t tx = 0; + uint32_t ty = 0; + + // Set a pointer for textures later + paletteColor_t* texture = NULL; + // The ceiling texture is always this + paletteColor_t* ceilTexture = getTexByType(ray, BG_CEILING)->px; + + // Save these to not resolve pointers later + uint32_t mapW = ray->map.w; + uint32_t mapH = ray->map.h; + + // rayDir for leftmost ray (x = 0) and rightmost ray (x = w) + q24_8 rayDirX0 = SUB_FX(ray->dirX, ray->planeX); + q24_8 rayDirY0 = SUB_FX(ray->dirY, ray->planeY); + q24_8 rayDirX1 = ADD_FX(ray->dirX, ray->planeX); + q24_8 rayDirY1 = ADD_FX(ray->dirY, ray->planeY); + + // Loop through each horizontal row + for (int32_t y = firstRow; y < lastRow; y++) + { + // Are we casting on the floor or ceiling? + bool isFloor = y > (TFT_HEIGHT / 2); + + // Current y position compared to the center of the screen (the horizon) + int32_t p; + if (isFloor) + { + p = (y - (TFT_HEIGHT / 2)); + } + else + { + p = ((TFT_HEIGHT / 2) - y); + } + + // Don't divide by zero (infinite distance on the horizon) + if (0 == p) + { + continue; + } + + // Vertical position of the camera. + // NOTE: with 0.5, it's exactly in the center between floor and ceiling, + // matching also how the walls are being casted. For different values + // than 0.5, a separate loop must be done for ceiling and floor since + // they're no longer symmetrical. + q24_8 camZ; + if (isFloor) + { + camZ = ADD_FX(TO_FX_FRAC(TFT_HEIGHT, 2), ray->posZ); + } + else + { + camZ = SUB_FX(TO_FX_FRAC(TFT_HEIGHT, 2), ray->posZ); + } + + // Horizontal distance from the camera to the floor for the current row. + // 0.5 is the z position exactly in the middle between floor and ceiling. + // NOTE: this is affine texture mapping, which is not perspective correct + // except for perfectly horizontal and vertical surfaces like the floor. + // NOTE: this formula is explained as follows: The camera ray goes through + // the following two points: the camera itself, which is at a certain + // height (posZ), and a point in front of the camera (through an imagined + // vertical plane containing the screen pixels) with horizontal distance + // 1 from the camera, and vertical position p lower than posZ (posZ - p). When going + // through that point, the line has vertically traveled by p units and + // horizontally by 1 unit. To hit the floor, it instead needs to travel by + // posZ units. It will travel the same ratio horizontally. The ratio was + // 1 / p for going through the camera plane, so to go posZ times farther + // to reach the floor, we get that the total horizontal distance is posZ / p. + q24_8 rowDistance = camZ / p; + + // real world coordinates of the leftmost column. This will be updated as we step to the right. + q16_16 floorX = ADD_FX(ray->posX, MUL_FX(rowDistance, rayDirX0)) << EX_CEIL_PRECISION_BITS; + q16_16 floorY = ADD_FX(ray->posY, MUL_FX(rowDistance, rayDirY0)) << EX_CEIL_PRECISION_BITS; + + // calculate the real world step vector we have to add for each x (parallel to camera plane) + // adding step by step avoids multiplications with a weight in the inner loop + q16_16 floorStepX = (MUL_FX(rowDistance, SUB_FX(rayDirX1, rayDirX0)) << EX_CEIL_PRECISION_BITS) / TFT_WIDTH; + q16_16 floorStepY = (MUL_FX(rowDistance, SUB_FX(rayDirY1, rayDirY0)) << EX_CEIL_PRECISION_BITS) / TFT_WIDTH; + + // This is the fixed point amount each texture coordinate increments per screen pixel + q16_16 texStepX = TEX_WIDTH * floorStepX; + q16_16 texStepY = TEX_HEIGHT * floorStepY; + + // Loop through each pixel + for (int32_t x = 0; x < TFT_WIDTH; ++x) + { + // the cell coord is simply got from the integer parts of floorX and floorY + cellX = floorX >> Q16_16_FRAC_BITS; + cellY = floorY >> Q16_16_FRAC_BITS; + + // Only draw floor and ceiling for valid cells, otherwise leave the pixel as-is + if (cellX < mapW && cellY < mapH) + { + // If the cell changed + if ((cellX != lastCellX) || (cellY != lastCellY)) + { + // Record the change + lastCellX = cellX; + lastCellY = cellY; + + if (isFloor) + { + // Get the next cell texture + rayMapCellType_t type = ray->map.tiles[cellX][cellY].type; + // Always draw floor under doors + if (CELL_IS_TYPE(type, BG | DOOR)) + { + type = BG_FLOOR; + } + texture = getTexByType(ray, type)->px; + } + else + { + texture = ceilTexture; + } + + // get the texture coordinate from the fractional part + texPosX = (TEX_WIDTH * (floorX - (floorX & Q16_16_WHOLE_MASK))); + texPosY = (TEX_HEIGHT * (floorY - (floorY & Q16_16_WHOLE_MASK))); + } + + // Get the integer texture indices from the fixed point texture position + tx = (((uint32_t)texPosX) >> Q16_16_FRAC_BITS) % TEX_WIDTH; + ty = (((uint32_t)texPosY) >> Q16_16_FRAC_BITS) % TEX_HEIGHT; + + // Draw the pixel + if (isXray) + { + TURBO_SET_PIXEL(x, y, xrayPaletteSwap[texture[TEX_WIDTH * ty + tx]]); + } + else + { + TURBO_SET_PIXEL(x, y, texture[TEX_WIDTH * ty + tx]); + } + } + + // Always increment, regardless of if pixels were drawn + floorX += floorStepX; + floorY += floorStepY; + + // Increment the texture coordinate as well + texPosX += texStepX; + texPosY += texStepY; + } + } +} + +/** + * @brief Draw all the wall pixels. Walls are drawn second. This iterates over the screen left-to-right and draws + * vertical columns. + * This is called from the main loop. + * + * @param ray The entire game state + */ +void castWalls(ray_t* ray) +{ + // We'll be drawing pixels, so set this up + SETUP_FOR_TURBO(); + + // Boolean if the colors should be drawn inverted + bool isXray = (LO_XRAY == ray->loadout); + + // For each ray + for (int32_t x = 0; x < TFT_WIDTH; x++) + { + // calculate ray position and direction + q24_8 cameraX = ((x * TO_FX(2)) / TFT_WIDTH) - TO_FX(1); // x-coordinate in camera space + q24_8 rayDirX = ADD_FX(ray->dirX, MUL_FX(ray->planeX, cameraX)); + q24_8 rayDirY = ADD_FX(ray->dirY, MUL_FX(ray->planeY, cameraX)); + + // which box of the map we're in + int32_t mapX = FROM_FX(ray->posX); + int32_t mapY = FROM_FX(ray->posY); + + // length of ray from one x or y-side to next x or y-side + // these are derived as: + // deltaDistX = sqrt(1 + (rayDirY * rayDirY) / (rayDirX * rayDirX)) + // deltaDistY = sqrt(1 + (rayDirX * rayDirX) / (rayDirY * rayDirY)) + // which can be simplified to abs(|rayDir| / rayDirX) and abs(|rayDir| / rayDirY) + // where |rayDir| is the length of the vector (rayDirX, rayDirY). Its length, + // unlike (ray->dirX, ray->dirY) is not 1, however this does not matter, only the + // ratio between deltaDistX and deltaDistY matters, due to the way the DDA + // stepping further below works. So the values can be computed as below. + // Division through zero is prevented + q24_8 deltaDistX = (rayDirX == 0) ? INT32_MAX : ABS(DIV_FX(TO_FX(1), rayDirX)); + q24_8 deltaDistY = (rayDirY == 0) ? INT32_MAX : ABS(DIV_FX(TO_FX(1), rayDirY)); + + // what direction to step in x or y-direction (either +1 or -1) + int32_t stepX = 0; + int32_t stepY = 0; + + // length of ray from current position to next x or y-side + q24_8 sideDistX = 0; + q24_8 sideDistY = 0; + + // calculate step and initial sideDist (x) + if (rayDirX < 0) + { + stepX = -1; + sideDistX = MUL_FX(SUB_FX(ray->posX, TO_FX(mapX)), deltaDistX); + } + else if (rayDirX > 0) + { + stepX = 1; + sideDistX = MUL_FX(SUB_FX(TO_FX(mapX + 1), ray->posX), deltaDistX); + } + + // calculate step and initial sideDist (y) + if (rayDirY < 0) + { + stepY = -1; + sideDistY = MUL_FX(SUB_FX(ray->posY, TO_FX(mapY)), deltaDistY); + } + else if (rayDirY > 0) + { + stepY = 1; + sideDistY = MUL_FX(SUB_FX(TO_FX(mapY + 1), ray->posY), deltaDistY); + } + + bool hit = false; // was there a wall hit? + bool side = false; // was a NS or a EW wall hit? + + q24_8 wallX = 0; // where exactly the wall was hit + int32_t lineHeight = 0, drawStart = 0, drawEnd = 0; // the height of the wall strip + bool xrayOverride = false; // Whether or not a wall should be drawn instead of a door + // perform DDA + while (false == hit) + { + // jump to next map square, either in x-direction, or in y-direction + if (sideDistX < sideDistY) + { + sideDistX = ADD_FX(sideDistX, deltaDistX); + mapX += stepX; + side = false; + } + else + { + sideDistY = ADD_FX(sideDistY, deltaDistY); + mapY += stepY; + side = true; + } + + // Check if ray has hit a wall or door + rayMapCellType_t tileType = ray->map.tiles[mapX][mapY].type; + if (CELL_IS_TYPE(tileType, BG | WALL) || CELL_IS_TYPE(tileType, BG | DOOR)) + { + // Check if the door should be drawn recessed or not + bool drawRecessedDoor = false; + if (tileType == BG_DOOR_XRAY) + { + // X-Ray door, only draw recessed if the X-Ray loadout is active or the door is open + if ((LO_XRAY == ray->loadout) || (TO_FX(1) == ray->map.tiles[mapX][mapY].doorOpen)) + { + // Draw recessed door + drawRecessedDoor = true; + } + // If not fully open, draw X-Ray doors as walls without the X-Ray loadout + else + { + xrayOverride = true; + } + } + else if (CELL_IS_TYPE(tileType, BG | DOOR)) + { + // Not an X-Ray door, always draw recessed + drawRecessedDoor = true; + } + + // If this cell is a door + if (drawRecessedDoor) + { + // Check if the ray actually intersects the recessed door + if (rayIntersectsDoor(side, mapX, mapY, ray->posX, ray->posY, rayDirX, rayDirY, deltaDistX, + deltaDistY, ray->map.tiles[mapX][mapY].doorOpen)) + { + // Add a half step to these values to recess the door + sideDistX = ADD_FX(sideDistX, deltaDistX / 2); + sideDistY = ADD_FX(sideDistY, deltaDistY / 2); + } + else + { + // Didn't collide with a door, so keep DDA'ing + continue; + } + } + + // Calculate distance projected on camera direction. This is the shortest distance from the point + // where the wall is hit to the camera plane. Euclidean to center camera point would give fisheye + // effect! This can be computed as (mapX - ray->posX + (1 - stepX) / 2) / rayDirX for side == 0, or + // same formula with Y for size == 1, but can be simplified to the code below thanks to how sideDist + // and deltaDist are computed: because they were left scaled to |rayDir|. sideDist is the entire + // length of the ray above after the multiple steps, but we subtract deltaDist once because one step + // more into the wall was taken above. + q24_8 perpWallDist; + if (false == side) + { + perpWallDist = SUB_FX(sideDistX, deltaDistX); + } + else + { + perpWallDist = SUB_FX(sideDistY, deltaDistY); + } + + // Save the distance to this wall strip, used for sprite casting + ray->wallDistBuffer[x] = perpWallDist; + + if (perpWallDist == 0) + { + // Calculate height of line to draw on screen, make sure not to div by zero + lineHeight = TFT_HEIGHT; + + // calculate lowest and highest pixel to fill in current stripe + drawStart = (TFT_HEIGHT - lineHeight) / 2; + drawEnd = (TFT_HEIGHT + lineHeight) / 2; + } + else + { + // Calculate height of line to draw on screen, make sure not to div by zero + lineHeight = TO_FX(TFT_HEIGHT) / perpWallDist; + + // calculate lowest and highest pixel to fill in current stripe + drawStart = (TFT_HEIGHT - lineHeight) / 2 + (ray->posZ / perpWallDist); + drawEnd = (TFT_HEIGHT + lineHeight) / 2 + (ray->posZ / perpWallDist); + } + + // Sometimes textures wraparound b/c the math for wallX comes out to be like + // 19.003 -> 0 + // 19.000 -> 0 + // 18.995 -> 63 + + // calculate value of wallX + if (false == side) + { + wallX = ADD_FX(ray->posY, MUL_FX(perpWallDist, rayDirY)); + } + else + { + wallX = ADD_FX(ray->posX, MUL_FX(perpWallDist, rayDirX)); + } + wallX = SUB_FX(wallX, FLOOR_FX(wallX)); + + // For sliding doors + if (drawRecessedDoor) + { + // Adjust wallX to start drawing the texture at the door's edge rather than the map cell's edge + wallX -= ray->map.tiles[mapX][mapY].doorOpen; + + // If this is negative, it would draw an out-of-bounds pixel. + // Negative numbers are a rounding error, so make it zero + if (wallX < 0) + { + wallX = 0; + } + } + + // Wall or door was hit, this stops the DDA loop + hit = true; + } + } + + // x coordinate on the texture + int32_t texX = FROM_FX(wallX * TEX_WIDTH); + + // Mirror X texture coordinate for certain walls + if ((false == side && rayDirX < 0) || (true == side && rayDirY > 0)) + { + texX = TEX_WIDTH - texX - 1; + } + + // How much to increase the texture coordinate per screen pixel + q8_24 step = (TEX_HEIGHT << 24) / lineHeight; + + // Starting texture coordinate. If it would start offscreen, start it at the right spot onscreen instead + q8_24 texPos = 0; + if (drawStart < 0) + { + // Start the texture somewhere in the middle + texPos = (-drawStart) * step; + // Always start drawing on screen + drawStart = 0; + } + + // Also make sure to not draw off the bottom of the display + if (drawEnd > TFT_HEIGHT) + { + drawEnd = TFT_HEIGHT; + } + + // Pick the texture based on the map tile + paletteColor_t* tex; + if (xrayOverride) + { + tex = getTexByType(ray, BG_WALL_1)->px; + } + else + { + tex = getTexByType(ray, ray->map.tiles[mapX][mapY].type)->px; + } + + // Draw a vertical strip + for (int32_t y = drawStart; y < drawEnd; y++) + { + // Cast the texture coordinate to integer, and mod it to ensure no out of bounds reads + int32_t texY = (texPos >> 24) % TEX_HEIGHT; + + // Increment the texture position for the next iteration + texPos += step; + + // Get the color from the texture + + if (isXray) + { + TURBO_SET_PIXEL(x, y, xrayPaletteSwap[tex[TEX_HEIGHT * texY + texX]]); + } + else + { + TURBO_SET_PIXEL(x, y, tex[TEX_HEIGHT * texY + texX]); + } + } + } +} + +/** + * @brief Check if a ray intersects with a door. Doors are recessed a half-cell back + * + * @param side true if the ray came through horizontal boundary, false if it came through a vertical boundary + * @param mapX The player's current map cell X + * @param mapY The player's current map cell Y + * @param posX The player's current position X, fixed point decimal + * @param posY The player's current position Y, fixed point decimal + * @param rayDirX The X component of the ray being cast, fixed point decimal + * @param rayDirY The Y component of the ray being cast, fixed point decimal + * @param deltaDistX The X component of a DDA step, fixed point decimal + * @param deltaDistY The Y component of a DDA step, fixed point decimal + * @param doorOpen How open the door is, 0 to 1, fixed point decimal + * @return true if the ray intersects the door, false if it doesn't + */ +static bool rayIntersectsDoor(bool side, int32_t mapX, int32_t mapY, q24_8 posX, q24_8 posY, q24_8 rayDirX, + q24_8 rayDirY, q24_8 deltaDistX, q24_8 deltaDistY, q24_8 doorOpen) +{ + // Avoid division by zero + if (0 == rayDirX) + { + // Compare the decimal part of the camera X position to how open the door is + if ((posX & (TO_FX(1) - 1)) >= doorOpen) + { + // Intersection! + return true; + } + } + else + { + // Find the B part of a line formula (y = m*x + b) + q24_8 playerB = SUB_FX(posY, DIV_FX(MUL_FX(posX, rayDirY), rayDirX)); + + // Do different checks whether the ray is coming through a horizontal or vertical boundary + if (side) + { + // Check for intersection with 'horizontal' door, i.e. fixed Y + // Avoid division by zero + if (0 == rayDirY) + { + // Compare the decimal part of the camera Y position to how open the door is + if ((posY & (TO_FX(1) - 1)) >= doorOpen) + { + // Intersection + return true; + } + } + else + { + // Find the door's Y so we can solve for X (recess it half a cell) + q24_8 doorY; + if (deltaDistY > 0) + { + doorY = TO_FX(mapY) + TO_FX_FRAC(1, 2); + } + else + { + doorY = TO_FX(mapY) - TO_FX_FRAC(1, 2); + } + + // Solve for the X of the intersection + q24_8 doorIntersectionX = DIV_FX(MUL_FX(SUB_FX(doorY, playerB), rayDirX), rayDirY); + + // If the intersection is in the same cell as the door + if (FROM_FX(doorIntersectionX) == mapX) + { + // Compare the decimal part of the intersection point with how open the door is + if ((doorIntersectionX & (TO_FX(1) - 1)) >= doorOpen) + { + // The ray intersects with the door + return true; + } + } + } + } + else + { + // 'Vertical' door, fixed X + + // Find the door's X so we can solve for Y (recess it half a cell) + q24_8 doorX; + if (deltaDistX > 0) + { + doorX = TO_FX(mapX) + TO_FX_FRAC(1, 2); + } + else + { + doorX = TO_FX(mapX) - TO_FX_FRAC(1, 2); + } + + // Solve for the Y of the intersection + q24_8 doorIntersectionY = ADD_FX(DIV_FX(MUL_FX(doorX, rayDirY), rayDirX), playerB); + + // If the intersection is in the same cell as the door + if (FROM_FX(doorIntersectionY) == mapY) + { + // Compare the decimal part of the intersection point with how open the door is + if ((doorIntersectionY & (TO_FX(1) - 1)) >= doorOpen) + { + // The ray intersects with the door + return true; + } + } + } + } + + // No intersection + return false; +} + +/** + * @brief Compare two objDist_t* based on distance + * + * @param obj1 A objDist_t* to compare + * @param obj2 Another objDist_t* to compare + * @return an integer less than, equal to, or greater than zero if the first + * argument is considered to be respectively less than, equal to, or + * greater than the second. + */ +static int objDistComparator(const void* obj1, const void* obj2) +{ + return (((const objDist_t*)obj2)->dist - ((const objDist_t*)obj1)->dist); +} + +/** + * @brief Draw all the sprites. Sprites are drawn third. This sorts all sprites from furthest away to closest, then + * draws them on the screen. + * This is called from the main loop. + * + * @param ray The entire game state + * @return The closest sprite in the lock zone, i.e. center of the display. This may be NULL if there are no centered + * sprites. + */ +rayObjCommon_t* castSprites(ray_t* ray) +{ + rayObjCommon_t* lockedObj = NULL; + + // Setup to draw + SETUP_FOR_TURBO(); + + // Boolean if the colors should be drawn inverted + bool isXray = (LO_XRAY == ray->loadout); + + // Put an array on the stack to sort all sprites + objDist_t allObjs[MAX_RAY_BULLETS + ray->scenery.length + ray->enemies.length + ray->items.length]; + int32_t allObjsIdx = 0; + + // For convenience + q24_8 rayPosX = ray->posX; + q24_8 rayPosY = ray->posY; + + // Assign each bullet a distance from the player + for (int i = 0; i < MAX_RAY_BULLETS; i++) + { + // Make a convenience pointer + rayBullet_t* obj = &ray->bullets[i]; + if (-1 != obj->c.id) + { + // Save the pointer and the distance to sort + allObjs[allObjsIdx].obj = &obj->c; + q24_8 delX = rayPosX - obj->c.posX; + q24_8 delY = rayPosY - obj->c.posY; + allObjs[allObjsIdx].dist = (delX * delX) + (delY * delY); + allObjsIdx++; + } + } + + // Assign each enemy a distance from the player + node_t* currentNode = ray->enemies.first; + while (currentNode != NULL) + { + // Get a pointer from the linked list + rayEnemy_t* obj = ((rayEnemy_t*)currentNode->val); + + // Save the pointer and the distance to sort + allObjs[allObjsIdx].obj = &obj->c; + q24_8 delX = rayPosX - obj->c.posX; + q24_8 delY = rayPosY - obj->c.posY; + allObjs[allObjsIdx].dist = (delX * delX) + (delY * delY); + allObjsIdx++; + + // Iterate to the next node + currentNode = currentNode->next; + } + + // Assign each enemy a distance from the player + currentNode = ray->scenery.first; + while (currentNode != NULL) + { + // Get a pointer from the linked list + rayObjCommon_t* obj = ((rayObjCommon_t*)currentNode->val); + + // Save the pointer and the distance to sort + allObjs[allObjsIdx].obj = obj; + q24_8 delX = rayPosX - obj->posX; + q24_8 delY = rayPosY - obj->posY; + allObjs[allObjsIdx].dist = (delX * delX) + (delY * delY); + allObjsIdx++; + + // Iterate to the next node + currentNode = currentNode->next; + } + + // Assign each item a distance from the player + currentNode = ray->items.first; + while (currentNode != NULL) + { + // Get a pointer from the linked list + rayObjCommon_t* obj = ((rayObjCommon_t*)currentNode->val); + + // Save the pointer and the distance to sort + allObjs[allObjsIdx].obj = obj; + q24_8 delX = rayPosX - obj->posX; + q24_8 delY = rayPosY - obj->posY; + allObjs[allObjsIdx].dist = (delX * delX) + (delY * delY); + allObjsIdx++; + + // Iterate to the next node + currentNode = currentNode->next; + } + + // Sort the sprites by distance + qsort(allObjs, allObjsIdx, sizeof(objDist_t), objDistComparator); + + // after sorting the sprites, do the projection and draw them + for (int i = 0; i < allObjsIdx; i++) + { + // Make a convenience pointer + rayObjCommon_t* obj = allObjs[i].obj; + + // Make sure this object slot is occupied + if (-1 != obj->id) + { + // Get WSG dimensions for convenience + uint32_t tWidth = obj->sprite->w; + uint32_t tHeight = obj->sprite->h; + + // translate sprite position to relative to camera + q24_8 spriteX = SUB_FX(obj->posX, ray->posX); + q24_8 spriteY = SUB_FX(obj->posY, ray->posY); + + // transform sprite with the inverse camera matrix + // [ planeX dirX ] -1 [ dirY -dirX ] + // [ ] = 1/(planeX*dirY-dirX*planeY) * [ ] + // [ planeY dirY ] [ -planeY planeX ] + + // required for correct matrix multiplication + q24_8 invDetDivisor = SUB_FX(MUL_FX(ray->planeX, ray->dirY), MUL_FX(ray->dirX, ray->planeY)); + + // this is actually the depth inside the screen, that what Z is in 3D + q24_8 transformY + = DIV_FX(ADD_FX(MUL_FX(-ray->planeY, spriteX), MUL_FX(ray->planeX, spriteY)), invDetDivisor); + + // If this is negative, the texture isn't going to be drawn, so just stop here + if (transformY <= 0) + { + // Not drawn in bounds, so continue to the next object + continue; + } + + // Do all the X math first to see if its on screen, then do Y math?? + q24_8 transformX = DIV_FX(SUB_FX(MUL_FX(ray->dirY, spriteX), MUL_FX(ray->dirX, spriteY)), invDetDivisor); + + // The center of the sprite in screen space + // The division here takes the number from q24_8 to int32_t + int32_t spriteScreenX = (TFT_WIDTH * (transformX + transformY)) / (2 * transformY); + + // The width of the screen area to draw the sprite into, in pixels + int32_t spriteWidth = (tWidth * TO_FX(TFT_HEIGHT)) / (TEX_WIDTH * transformY); + if (0 == spriteWidth) + { + // If this sprite has zero width, don't draw it + continue; + } + // Width should always be positive + if (spriteWidth < 0) + { + spriteWidth = -spriteWidth; + } + + // This is the texture step per-screen-pixel + q16_16 texXDelta = (tWidth << 16) / spriteWidth; + // This is the inital texture X coordinate + q16_16 texX = 0; + + // Find the pixel X coordinate where the sprite draw starts. It may be negative + int32_t drawStartX = spriteScreenX - (spriteWidth / 2); + // If the sprite would start to draw off-screen + if (drawStartX < 0) + { + // Advance the initial texture X coordinate by the difference + texX = texXDelta * -drawStartX; + // Start drawing at the screen edge + drawStartX = 0; + } + // Find the pixel X coordinate where the sprite draw ends. It may be off the screen + int32_t drawEndX = spriteScreenX + (spriteWidth / 2); + if (drawEndX > TFT_WIDTH) + { + // Always stop drawing at the screen edge + drawEndX = TFT_WIDTH; + } + + if (drawStartX >= TFT_WIDTH || drawEndX < 0) + { + // Not drawn in bounds, so continue to the next object + continue; + } + + // Mirrored sprites draw backwards + if (obj->spriteMirrored) + { + texXDelta = -texXDelta; + texX = (tWidth << 16) - texX - 1; + } + + // Adjust the sprite draw based on the vertical camera height. + // Dividing two q24_8 variables gets a int32_t + int32_t spritePosZ = ray->posZ / transformY; + + // calculate height of the sprite on screen + // using 'transformY' instead of the real distance prevents fisheye + int32_t spriteHeight = TO_FX(TFT_HEIGHT) / transformY; + if (spriteHeight < 0) + { + spriteHeight = -spriteHeight; + } + + // This is the texture step per-screen-pixel + q16_16 texYDelta = (tHeight << 16) / spriteHeight; + // This is the inital texture Y coordinate + q16_16 initialTexY = 0; + + // Find the pixel Y coordinate where the sprite draw starts. It may be negative + int32_t drawStartY = (-spriteHeight + TFT_HEIGHT) / 2 + spritePosZ; + if (drawStartY < 0) + { + // Advance the initial texture Y coordinate by the difference + initialTexY = texYDelta * -drawStartY; + // Start drawing at the screen edge + drawStartY = 0; + } + + // Find the pixel Y coordinate where the sprite draw ends. It may be off the screen + int32_t drawEndY = (spriteHeight + TFT_HEIGHT) / 2 + spritePosZ; + if (drawEndY > TFT_HEIGHT) + { + // Always stop drawing at the screen edge + drawEndY = TFT_HEIGHT; + } + + // loop through every vertical stripe of the sprite on screen + for (int32_t stripe = drawStartX; stripe < drawEndX; stripe++) + { + // Check wallDistBuffer to make sure the sprite is on the screen + if (transformY < ray->wallDistBuffer[stripe]) + { + // Check if this should be locked onto + if (((TFT_WIDTH / 2) - LOCK_ZONE) <= stripe && stripe <= ((TFT_WIDTH / 2) + LOCK_ZONE)) + { + // Closest sprites are drawn last, so override the lock + lockedObj = obj; + } + + // Reset the texture Y coordinate + q16_16 texY = initialTexY; + + // for every pixel of the current stripe + for (int32_t y = drawStartY; y < drawEndY; y++) + { + // get current color from the texture, draw if not transparent + paletteColor_t color = obj->sprite->px[tWidth * (texY >> 16) + (texX >> 16)]; + if (cTransparent != color) + { + if (isXray) + { + TURBO_SET_PIXEL(stripe, y, xrayPaletteSwap[color]); + } + else + { + TURBO_SET_PIXEL(stripe, y, color); + } + } + texY += texYDelta; + } + } + texX += texXDelta; + } + } + } + return lockedObj; +} + +/** + * @brief Draw the HUD on the display based on the player loadout. This is drawn last. + * This is called from the main loop. + * + * @param ray The entire game state + */ +void drawHud(ray_t* ray) +{ + if (LO_NONE != ray->loadout) + { + wsg_t* gun = &ray->guns[ray->loadout]; + int32_t yOffset = TFT_HEIGHT - gun->h; + // If a loadout change is in progress + if (ray->loadoutChangeTimer) + { + // Loadout is changing out + if (ray->loadout != ray->nextLoadout) + { + // Timer goes from LOADOUT_TIMER_US to 0, as it gets smaller the gun moves down + yOffset += (gun->h - ((ray->loadoutChangeTimer * gun->h) / LOADOUT_TIMER_US)); + } + // Loadout is changing in + else + { + // Timer goes from LOADOUT_TIMER_US to 0, as it gets smaller the gun moves up + yOffset += ((ray->loadoutChangeTimer * gun->h) / LOADOUT_TIMER_US); + } + } + drawWsgSimple(gun, TFT_WIDTH - gun->w, yOffset); + } + + // If the player has missiles + if (ray->inventory.missileLoadOut) + { + // Draw a count of missiles + char missileStr[16] = {0}; + snprintf(missileStr, sizeof(missileStr) - 1, "%03" PRId32 "/%03" PRId32, ray->inventory.numMissiles, + ray->inventory.maxNumMissiles); + drawText(&ray->ibm, c555, missileStr, 64, TFT_HEIGHT - ray->ibm.height); + } + +#define BAR_END_MARGIN 40 +#define BAR_SIDE_MARGIN 8 +#define BAR_WIDTH 8 + // Find the width of the entire health bar + int32_t maxHealthWidth = (ray->inventory.maxHealth * (TFT_WIDTH - (BAR_END_MARGIN * 2))) / MAX_HEALTH_EVER; + // Find the width of the filled part of the health bar + int32_t currHealthWidth = (ray->inventory.health * (TFT_WIDTH - (BAR_END_MARGIN * 2))) / MAX_HEALTH_EVER; + // Draw a health bar + fillDisplayArea(BAR_END_MARGIN, // + BAR_SIDE_MARGIN, // + BAR_END_MARGIN + currHealthWidth, // + BAR_SIDE_MARGIN + BAR_WIDTH, // + c030); + fillDisplayArea(BAR_END_MARGIN + currHealthWidth, // + BAR_SIDE_MARGIN, // + BAR_END_MARGIN + maxHealthWidth, // + BAR_SIDE_MARGIN + BAR_WIDTH, // + c400); + + // Draw charge beam indicator + int32_t chargeIndicatorStart + = TFT_HEIGHT - BAR_END_MARGIN - ((ray->chargeTimer * (TFT_HEIGHT - (2 * BAR_END_MARGIN))) / CHARGE_TIME_US); + fillDisplayArea(BAR_SIDE_MARGIN, // + chargeIndicatorStart, // + BAR_SIDE_MARGIN + BAR_WIDTH, // + TFT_HEIGHT - BAR_END_MARGIN, // + c550); + fillDisplayArea(TFT_WIDTH - BAR_SIDE_MARGIN - BAR_WIDTH, // + chargeIndicatorStart, // + TFT_WIDTH - BAR_SIDE_MARGIN, // + TFT_HEIGHT - BAR_END_MARGIN, // + c550); + + // Draw side bars according to suit colors + paletteColor_t sideBarColor = c432; + if (ray->inventory.waterSuit) + { + sideBarColor = c223; + } + else if (ray->inventory.lavaSuit) + { + sideBarColor = c510; + } + fillDisplayArea(BAR_SIDE_MARGIN, // + BAR_END_MARGIN, // + BAR_SIDE_MARGIN + BAR_WIDTH, // + chargeIndicatorStart, // + sideBarColor); + fillDisplayArea(TFT_WIDTH - BAR_SIDE_MARGIN - BAR_WIDTH, // + BAR_END_MARGIN, // + TFT_WIDTH - BAR_SIDE_MARGIN, // + chargeIndicatorStart, // + sideBarColor); +} + +/** + * @brief Run all environment timers, including door openings and head-bob + * + * @param ray The entire game state + * @param elapsedUs The elapsed time since this function was last called + */ +void runEnvTimers(ray_t* ray, uint32_t elapsedUs) +{ + // Run a timer for head bob + ray->bobTimer += elapsedUs; + while (ray->bobTimer > 2500) + { + ray->bobTimer -= 2500; + + // Only bob when walking or finishing a bob cycle + if ((ray->btnState & (PB_UP | PB_DOWN)) || (0 != ray->bobCount && 180 != ray->bobCount)) + { + // Step through the bob cycle, which is a sin function + ray->bobCount++; + if (360 == ray->bobCount) + { + ray->bobCount = 0; + } + // Bob the camera. Note that fixed point numbers are << 8, and trig functions are << 10 + ray->posZ = getSin1024(ray->bobCount) * 4; + } + else + { + // Reset the count to always restart on an upward bob + ray->bobCount = 0; + } + } + + // Run a timer to open and close doors + ray->doorTimer += elapsedUs; + while (ray->doorTimer >= 5000) + { + ray->doorTimer -= 5000; + + for (int32_t y = 0; y < ray->map.h; y++) + { + for (int32_t x = 0; x < ray->map.w; x++) + { + if (ray->map.tiles[x][y].doorOpen > 0 && ray->map.tiles[x][y].doorOpen < TO_FX(1)) + { + ray->map.tiles[x][y].doorOpen++; + } + } + } + } +} diff --git a/main/modes/ray/ray_renderer.h b/main/modes/ray/ray_renderer.h new file mode 100644 index 000000000..fad5e2f9f --- /dev/null +++ b/main/modes/ray/ray_renderer.h @@ -0,0 +1,17 @@ +#ifndef _RAY_RENDERER_H_ +#define _RAY_RENDERER_H_ + +#include +#include + +#define TEX_WIDTH 64 +#define TEX_HEIGHT 64 + +void runEnvTimers(ray_t* ray, uint32_t elapsedUs); + +void castFloorCeiling(ray_t* ray, int32_t firstRow, int32_t lastRow); +void castWalls(ray_t* ray); +rayObjCommon_t* castSprites(ray_t* ray); +void drawHud(ray_t* ray); + +#endif \ No newline at end of file diff --git a/main/modes/ray/ray_tex_manager.c b/main/modes/ray/ray_tex_manager.c new file mode 100644 index 000000000..cc03f5340 --- /dev/null +++ b/main/modes/ray/ray_tex_manager.c @@ -0,0 +1,162 @@ +//============================================================================== +// Includes +//============================================================================== + +#include +#include + +#include "macros.h" +#include "ray_tex_manager.h" +#include "ray_object.h" + +//============================================================================== +// Defines +//============================================================================== + +/** + * The maximum number of loaded sprites. + * TODO pick a better number for all textures + */ +#define MAX_LOADED_TEXTURES 128 + +/// Helper macro to load textures +#define LOAD_TEXTURE(r, t) loadTexture(r, #t ".wsg", t) + +//============================================================================== +// Functions +//============================================================================== + +/** + * @brief Allocate memory and preload all environment textures + * + * @param ray The ray_t to load textures for + */ +void loadEnvTextures(ray_t* ray) +{ + // Load HUD textures + loadWsg("GUN_NORMAL.wsg", &ray->guns[LO_NORMAL], true); + loadWsg("GUN_MISSILE.wsg", &ray->guns[LO_MISSILE], true); + loadWsg("GUN_ICE.wsg", &ray->guns[LO_ICE], true); + loadWsg("GUN_XRAY.wsg", &ray->guns[LO_XRAY], true); + + // Allocate space for the textures + ray->loadedTextures = heap_caps_calloc(MAX_LOADED_TEXTURES, sizeof(namedTexture_t), MALLOC_CAP_SPIRAM); + // Types are 8 bit, non sequential, so allocate a 256 element array. Probably too many + ray->typeToIdxMap = heap_caps_calloc(256, sizeof(uint8_t), MALLOC_CAP_SPIRAM); + + // Load everything by type! + LOAD_TEXTURE(ray, BG_FLOOR); + LOAD_TEXTURE(ray, BG_FLOOR_WATER); + LOAD_TEXTURE(ray, BG_FLOOR_LAVA); + LOAD_TEXTURE(ray, BG_CEILING); + LOAD_TEXTURE(ray, BG_WALL_1); + LOAD_TEXTURE(ray, BG_WALL_2); + LOAD_TEXTURE(ray, BG_WALL_3); + LOAD_TEXTURE(ray, BG_DOOR); + LOAD_TEXTURE(ray, BG_DOOR_CHARGE); + LOAD_TEXTURE(ray, BG_DOOR_MISSILE); + LOAD_TEXTURE(ray, BG_DOOR_ICE); + LOAD_TEXTURE(ray, BG_DOOR_XRAY); + LOAD_TEXTURE(ray, BG_DOOR_SCRIPT); + LOAD_TEXTURE(ray, OBJ_ITEM_BEAM); + LOAD_TEXTURE(ray, OBJ_ITEM_CHARGE_BEAM); + LOAD_TEXTURE(ray, OBJ_ITEM_MISSILE); + LOAD_TEXTURE(ray, OBJ_ITEM_ICE); + LOAD_TEXTURE(ray, OBJ_ITEM_XRAY); + LOAD_TEXTURE(ray, OBJ_ITEM_SUIT_WATER); + LOAD_TEXTURE(ray, OBJ_ITEM_SUIT_LAVA); + LOAD_TEXTURE(ray, OBJ_ITEM_ENERGY_TANK); + LOAD_TEXTURE(ray, OBJ_ITEM_KEY); + LOAD_TEXTURE(ray, OBJ_ITEM_ARTIFACT); + LOAD_TEXTURE(ray, OBJ_ITEM_PICKUP_ENERGY); + LOAD_TEXTURE(ray, OBJ_ITEM_PICKUP_MISSILE); + LOAD_TEXTURE(ray, OBJ_BULLET_NORMAL); + LOAD_TEXTURE(ray, OBJ_BULLET_CHARGE); + LOAD_TEXTURE(ray, OBJ_BULLET_ICE); + LOAD_TEXTURE(ray, OBJ_BULLET_MISSILE); + LOAD_TEXTURE(ray, OBJ_BULLET_XRAY); + LOAD_TEXTURE(ray, OBJ_SCENERY_TERMINAL); +} + +/** + * @brief Load a texture by name and set up a type mapping + * This will not load a texture if it's already loaded + * + * @param ray The ray_t to load a texture into + * @param wsgName The name of the texture to load + * @param type The type for this texture + * @return The A pointer to the loaded texture + */ +wsg_t* loadTexture(ray_t* ray, const char* name, rayMapCellType_t type) +{ + // Iterate over the loaded textures + for (int32_t idx = 0; idx < MAX_LOADED_TEXTURES; idx++) + { + // Check if the name is NULL + if (NULL == ray->loadedTextures[idx].name) + { + // If so, we've reached the end and should load this texture + if (loadWsg(name, &ray->loadedTextures[idx].texture, true)) + { + // If the texture loads, save the name + ray->loadedTextures[idx].name = calloc(1, strlen(name) + 1); + memcpy(ray->loadedTextures[idx].name, name, strlen(name) + 1); + } + + // If this has a type + if (EMPTY != type) + { + // Set up mapping for later + ray->typeToIdxMap[type] = idx; + } + + // Return the pointer + return &ray->loadedTextures[idx].texture; + } + else if (0 == strcmp(ray->loadedTextures[idx].name, name)) + { + // Name matches, so return this loaded texture + return &ray->loadedTextures[idx].texture; + } + } + // Should be impossible to get here + ESP_LOGE("JSON", "Couldn't load texture"); + return NULL; +} + +/** + * @brief Get a texture by type + * + * @param ray The ray_t to get a texture from + * @param type The type to get a texture for + * @return A pointer to the texture + */ +wsg_t* getTexByType(ray_t* ray, rayMapCellType_t type) +{ + return &ray->loadedTextures[ray->typeToIdxMap[type]].texture; +} + +/** + * @brief Free all textures and associated memory + * + * @param ray The ray_t to free textures from + */ +void freeAllTex(ray_t* ray) +{ + freeWsg(&ray->guns[LO_NORMAL]); + freeWsg(&ray->guns[LO_MISSILE]); + freeWsg(&ray->guns[LO_ICE]); + freeWsg(&ray->guns[LO_XRAY]); + + for (int32_t idx = 0; idx < MAX_LOADED_TEXTURES; idx++) + { + // Check if the name is NULL + if (NULL != ray->loadedTextures[idx].name) + { + free(ray->loadedTextures[idx].name); + freeWsg(&ray->loadedTextures[idx].texture); + } + } + free(ray->loadedTextures); + free(ray->typeToIdxMap); +} diff --git a/main/modes/ray/ray_tex_manager.h b/main/modes/ray/ray_tex_manager.h new file mode 100644 index 000000000..a90ab0a9e --- /dev/null +++ b/main/modes/ray/ray_tex_manager.h @@ -0,0 +1,9 @@ +#pragma once + +#include "swadge2024.h" +#include "mode_ray.h" + +void loadEnvTextures(ray_t* ray); +wsg_t* loadTexture(ray_t* ray, const char* name, rayMapCellType_t type); +wsg_t* getTexByType(ray_t* ray, rayMapCellType_t type); +void freeAllTex(ray_t* ray); diff --git a/main/modes/soko/soko.c b/main/modes/soko/soko.c new file mode 100644 index 000000000..2fec4ed36 --- /dev/null +++ b/main/modes/soko/soko.c @@ -0,0 +1,731 @@ +#include + +#include "soko.h" +#include "soko_game.h" +#include "soko_gamerules.h" + +static void sokoMainLoop(int64_t elapsedUs); +static void sokoEnterMode(void); +static void sokoExitMode(void); +static void sokoMenuCb(const char* label, bool selected, uint32_t settingVal); +static void sokoLoadBinLevel(uint16_t levelIndex); +static void sokoLoadLevel(uint16_t); +static void sokoBackgroundDrawCallback(int16_t x, int16_t y, int16_t w, int16_t h, int16_t up, int16_t upNum); +static sokoTile_t sokoGetTileFromColor(paletteColor_t); +static sokoEntityType_t sokoGetEntityFromColor(paletteColor_t); +static int sokoFindIndex(soko_abs_t* self, int targetIndex); +static void sokoExtractLevelNamesAndIndeces(soko_abs_t* self); + +// strings +static const char sokoModeName[] = "Sokobanabokabon"; +static const char sokoResumeGameLabel[] = "returnitytoit"; +static const char sokoNewGameLabel[] = "startsitfresh"; + +// create the mode +swadgeMode_t sokoMode = { + .modeName = sokoModeName, + .wifiMode = NO_WIFI, + .overrideUsb = false, + .usesAccelerometer = false, + .usesThermometer = false, + .fnEnterMode = sokoEnterMode, + .fnExitMode = sokoExitMode, + .fnMainLoop = sokoMainLoop, + .fnAudioCallback = NULL, + .fnBackgroundDrawCallback = sokoBackgroundDrawCallback, + .fnEspNowRecvCb = NULL, + .fnEspNowSendCb = NULL, + .fnAdvancedUSB = NULL, +}; + +// soko_t* soko=NULL; +soko_abs_t* soko = NULL; + +extern const char* sokoLevelNames[] + = {"sk_overworld1.wsg", "sk_sticky_test.wsg", "sk_test1.wsg", "sk_test2.wsg", "sk_test3.wsg"}; + +extern const soko_var_t sokoLevelVariants[] + = {SOKO_OVERWORLD, SOKO_EULER, SOKO_CLASSIC, SOKO_CLASSIC, SOKO_LASERBOUNCE}; + +//@TODO: Remove this when all levels do binary loading and dynamic name loading. +extern const char* sokoBinLevelNames[] = { + "sk_binOverworld.bin", "warehouse.bin", "sk_sticky_test.bin", "sk_test1.bin", "sk_test3.bin", +}; + +static void sokoEnterMode(void) +{ + soko = calloc(1, sizeof(soko_abs_t)); + // Load a font + loadFont("ibm_vga8.font", &soko->ibm, false); + + // todo: move to convenience function for loading level data. Preferrebly in it's own file so contributors can futz + // with it with fewer git merge cases. + soko->levels[0] = "sk_testpuzzle.wsg"; + + // free a wsg that we never loaded... is bad. + loadWsg(soko->levels[0], &soko->levelWSG, true); // spiRAM cus only used during loading, not gameplay. + + // load sprite assets + soko->currentTheme = &soko->sokoDefaultTheme; + + // Default Theme + loadWsg("sk_player_down.wsg", &soko->sokoDefaultTheme.playerDownWSG, false); + loadWsg("sk_player_up.wsg", &soko->sokoDefaultTheme.playerUpWSG, false); + loadWsg("sk_player_left.wsg", &soko->sokoDefaultTheme.playerLeftWSG, false); + loadWsg("sk_player_right.wsg", &soko->sokoDefaultTheme.playerRightWSG, false); + loadWsg("sk_crate.wsg", &soko->sokoDefaultTheme.crateWSG, false); + loadWsg("sk_sticky_crate.wsg", &soko->sokoDefaultTheme.stickyCrateWSG, false); + + soko->sokoDefaultTheme.wallColor = c111; + soko->sokoDefaultTheme.floorColor = c444; + + // Overworld Theme + soko->overworldTheme.playerDownWSG = soko->sokoDefaultTheme.playerDownWSG; + soko->overworldTheme.playerUpWSG = soko->sokoDefaultTheme.playerUpWSG; + soko->overworldTheme.playerLeftWSG = soko->sokoDefaultTheme.playerLeftWSG; + soko->overworldTheme.playerRightWSG = soko->sokoDefaultTheme.playerRightWSG; + soko->overworldTheme.crateWSG = soko->sokoDefaultTheme.crateWSG; + soko->overworldTheme.stickyCrateWSG = soko->sokoDefaultTheme.stickyCrateWSG; + + soko->overworldTheme.wallColor = c121; + soko->overworldTheme.floorColor = c454; + + // Initialize the menu + soko->menu = initMenu(sokoModeName, sokoMenuCb); + soko->menuLogbookRenderer = initMenuLogbookRenderer(&soko->ibm); + + addSingleItemToMenu(soko->menu, sokoResumeGameLabel); + addSingleItemToMenu(soko->menu, sokoNewGameLabel); + + // Set the mode to menu mode + soko->screen = SOKO_MENU; + soko->state = SKS_INIT; +} + +static void sokoExitMode(void) +{ + // Deinitialize the menu + deinitMenu(soko->menu); + deinitMenuLogbookRenderer(soko->menuLogbookRenderer); + + // Free the font + freeFont(&soko->ibm); + + // free the level name file + freeTxt(soko->levelFileText); + + // free the level + freeWsg(&soko->levelWSG); + + // free sprites + freeWsg(&soko->sokoDefaultTheme.playerUpWSG); + freeWsg(&soko->sokoDefaultTheme.playerDownWSG); + freeWsg(&soko->sokoDefaultTheme.playerLeftWSG); + freeWsg(&soko->sokoDefaultTheme.playerRightWSG); + freeWsg(&soko->sokoDefaultTheme.crateWSG); + freeWsg(&soko->sokoDefaultTheme.stickyCrateWSG); + + // Free everything else + free(soko); +} + +static void sokoMenuCb(const char* label, bool selected, uint32_t settingVal) +{ + if (selected) + { + // placeholder. + if (label == sokoResumeGameLabel) + { + // load level. + // sokoLoadLevel(0); + soko->levelFileText = loadTxt("SK_LEVEL_LIST.txt", true); + sokoExtractLevelNamesAndIndeces(soko); + /* + for(int i = 0; i < 20; i++) + { + int ind = findIndex(soko,i); + + if(ind != -1) + { + printf("Found %d at %d:%s\n",i,ind,soko->levelNames[ind]); + } + else + { + printf("%d Not Found\n",i); + } + } + */ + sokoLoadBinLevel(0); + sokoInitGameBin(soko); + soko->screen = SOKO_LEVELPLAY; + } + else if (label == sokoNewGameLabel) + { + // load level. + sokoLoadLevel(0); + sokoInitGame(soko); + soko->screen = SOKO_LEVELPLAY; + } + } +} + +static void sokoMainLoop(int64_t elapsedUs) +{ + // Pick what runs and draws depending on the screen being displayed + switch (soko->screen) + { + case SOKO_MENU: + { + // Process button events + buttonEvt_t evt = {0}; + while (checkButtonQueueWrapper(&evt)) + { + // Pass button events to the menu + soko->menu = menuButton(soko->menu, evt); + } + + // Draw the menu + drawMenuLogbook(soko->menu, soko->menuLogbookRenderer, elapsedUs); + break; + } + case SOKO_LEVELPLAY: + { + // pass along to other gameplay, in other file + // Always process button events, regardless of control scheme, so the main menu button can be captured + buttonEvt_t evt = {0}; + while (checkButtonQueueWrapper(&evt)) + { + // Save the button state + soko->input.btnState = evt.state; + } + + // process input functions in input. + // Input will turn state into function calls into the game code, and handle complexities. + sokoPreProcessInput(&soko->input, elapsedUs); + // background had been drawn, input has been processed and functions called. Now do followup logic and draw + // level. gameplay loop + soko->gameLoopFunc(soko, elapsedUs); + break; + } + case SOKO_LOADNEWLEVEL: + { + sokoLoadLevel(soko->loadNewLevelIndex); + sokoInitNewLevel(soko, sokoLevelVariants[soko->loadNewLevelIndex]); + + soko->screen = SOKO_LEVELPLAY; + } + } +} + +void freeEntity(soko_abs_t* self, sokoEntity_t* entity) // Free internal entity structures +{ + if (entity->propFlag) + { + if (entity->properties->targetCount) + { + free(entity->properties->targetX); + free(entity->properties->targetY); + } + free(entity->properties); + entity->propFlag = false; + } + self->currentLevel.entityCount -= 1; +} + +void sokoLoadBinTiles(soko_abs_t* self, int byteCount) +{ + const int HEADER_BYTE_OFFSET = 2; + int totalTiles = self->currentLevel.width * self->currentLevel.height; + int tileIndex = 0; + self->currentLevel.entityCount = 0; + self->goalCount = 0; + for (int i = HEADER_BYTE_OFFSET; i < byteCount; i++) + { + if (self->levelBinaryData[i] == SKB_OBJSTART) // Objects in level data should be of the form SKB_OBJSTART, + // SKB_[Object Type], [Data Bytes] , SKB_OBJEND + { + int objX = (tileIndex - 1) % (self->currentLevel.width); // Look at the previous + int objY = (tileIndex - 1) / (self->currentLevel.width); + uint8_t flagByte, direction; + bool players, crates, sticky, trail, inverted; + int hp, targetX, targetY; + switch (self->levelBinaryData[i + 1]) // On creating entities, index should be advanced to the SKB_OBJEND + // byte so the post-increment moves to the next tile. + { + case SKB_COMPRESS: + i += 2; + break; // Not yet implemented + case SKB_PLAYER: + self->currentLevel.gameMode = self->levelBinaryData[i + 2]; + self->currentLevel.entities[self->currentLevel.entityCount].type = SKE_PLAYER; + self->currentLevel.entities[self->currentLevel.entityCount].x = objX; + self->currentLevel.entities[self->currentLevel.entityCount].y = objY; + self->soko_player = &self->currentLevel.entities[self->currentLevel.playerIndex]; + self->currentLevel.playerIndex = self->currentLevel.entityCount; + self->currentLevel.entityCount += 1; + i += 3; + break; + case SKB_CRATE: + flagByte = self->levelBinaryData[i + 2]; + sticky = !!(flagByte & (0x1 << 0)); + trail = !!(flagByte & (0x1 << 1)); + self->currentLevel.entities[self->currentLevel.entityCount].type + = sticky ? SKE_STICKY_CRATE : SKE_CRATE; + self->currentLevel.entities[self->currentLevel.entityCount].x = objX; + self->currentLevel.entities[self->currentLevel.entityCount].y = objY; + self->currentLevel.entities[self->currentLevel.entityCount].properties + = malloc(sizeof(sokoEntityProperties_t)); + self->currentLevel.entities[self->currentLevel.entityCount].propFlag = true; + self->currentLevel.entities[self->currentLevel.entityCount].properties->sticky = sticky; + self->currentLevel.entities[self->currentLevel.entityCount].properties->trail = trail; + self->currentLevel.entityCount += 1; + i += 3; + break; + case SKB_WARPINTERNAL: //[type][flags][hp][destx][desty] + flagByte = self->levelBinaryData[i + 2]; + crates = !!(flagByte & (0x1 << 0)); + hp = self->levelBinaryData[i + 3]; + targetX = self->levelBinaryData[i + 4]; + targetY = self->levelBinaryData[i + 5]; + + self->currentLevel.entities[self->currentLevel.entityCount].type = SKE_WARP; + self->currentLevel.entities[self->currentLevel.entityCount].x = objX; + self->currentLevel.entities[self->currentLevel.entityCount].y = objY; + self->currentLevel.entities[self->currentLevel.entityCount].properties + = malloc(sizeof(sokoEntityProperties_t)); + self->currentLevel.entities[self->currentLevel.entityCount].propFlag = true; + self->currentLevel.entities[self->currentLevel.entityCount].properties->crates = crates; + self->currentLevel.entities[self->currentLevel.entityCount].properties->hp = hp; + self->currentLevel.entities[self->currentLevel.entityCount].properties->targetX + = malloc(sizeof(uint8_t)); + self->currentLevel.entities[self->currentLevel.entityCount].properties->targetY + = malloc(sizeof(uint8_t)); + self->currentLevel.entities[self->currentLevel.entityCount].properties->targetCount = 1; + self->currentLevel.entityCount += 1; + i += 6; + break; + case SKB_WARPINTERNALEXIT: + flagByte = self->levelBinaryData[i + 2]; + + i += 2; // No data or properties in this object. + break; // Can be used later on for verifying valid warps from save files. + case SKB_WARPEXTERNAL: //[typep][flags][index] + // todo implement extraction of index value and which values should be used for auto-indexed portals + self->currentLevel.tiles[objX][objY] = SKT_PORTAL; + self->portals[self->portalCount].index + = self->portalCount + 1; // For basic test, 1 indexed with levels, but multi-room overworld + // needs more sophistication to keep indeces correct. + self->portals[self->portalCount].x = objX; + self->portals[self->portalCount].y = objY; + printf("Portal %d at %d,%d\n", self->portals[self->portalCount].index, + self->portals[self->portalCount].x, self->portals[self->portalCount].y); + soko->portalCount += 1; + i += 4; + break; + case SKB_BUTTON: //[type][flag][numTargets][targetx][targety]... + flagByte = self->levelBinaryData[i + 2]; + crates = !!(flagByte & (0x1 << 0)); + players = !!(flagByte & (0x1 << 1)); + inverted = !!(flagByte & (0x1 << 2)); + sticky = !!(flagByte & (0x1 << 3)); + hp = self->levelBinaryData[i + 3]; + + self->currentLevel.entities[self->currentLevel.entityCount].type = SKE_BUTTON; + self->currentLevel.entities[self->currentLevel.entityCount].x = objX; + self->currentLevel.entities[self->currentLevel.entityCount].y = objY; + self->currentLevel.entities[self->currentLevel.entityCount].properties + = malloc(sizeof(sokoEntityProperties_t)); + self->currentLevel.entities[self->currentLevel.entityCount].propFlag = true; + self->currentLevel.entities[self->currentLevel.entityCount].properties->targetX + = malloc(sizeof(uint8_t) * hp); + self->currentLevel.entities[self->currentLevel.entityCount].properties->targetY + = malloc(sizeof(uint8_t) * hp); + self->currentLevel.entities[self->currentLevel.entityCount].properties->targetCount = true; + self->currentLevel.entities[self->currentLevel.entityCount].properties->crates = crates; + self->currentLevel.entities[self->currentLevel.entityCount].properties->players = players; + self->currentLevel.entities[self->currentLevel.entityCount].properties->inverted = inverted; + self->currentLevel.entities[self->currentLevel.entityCount].properties->sticky = sticky; + for (int j = 0; j < hp; j++) + { + self->currentLevel.entities[self->currentLevel.entityCount].properties->targetX[j] + = self->levelBinaryData[3 + 2 * j + 1]; + self->currentLevel.entities[self->currentLevel.entityCount].properties->targetY[j] + = self->levelBinaryData[3 + 2 * (j + 1)]; + } + self->currentLevel.entities[self->currentLevel.entityCount].properties->targetCount = hp; + self->currentLevel.entities[self->currentLevel.entityCount].properties->players = players; + self->currentLevel.entities[self->currentLevel.entityCount].properties->crates = crates; + self->currentLevel.entities[self->currentLevel.entityCount].properties->inverted = inverted; + self->currentLevel.entities[self->currentLevel.entityCount].properties->sticky = sticky; + self->currentLevel.entityCount += 1; + i += (4 + 2 * hp); + break; + case SKB_LASEREMITTER: //[type][flag] + flagByte = self->levelBinaryData[i + 2]; + direction = (flagByte & (0x3 << 6)) >> 6; // flagbyte stores direction in 0bDD0000P0 Where D is + // direction bits and P is player push + players = !!(flagByte & (0x1 < 1)); + + self->currentLevel.entities[self->currentLevel.entityCount].type = SKE_LASER_EMIT_UP; + self->currentLevel.entities[self->currentLevel.entityCount].x = objX; + self->currentLevel.entities[self->currentLevel.entityCount].y = objY; + self->currentLevel.entities[self->currentLevel.entityCount].facing = direction; + self->currentLevel.entities[self->currentLevel.entityCount].properties + = malloc(sizeof(sokoEntityProperties_t)); + self->currentLevel.entities[self->currentLevel.entityCount].propFlag = true; + self->currentLevel.entities[self->currentLevel.entityCount].properties->players = players; + self->currentLevel.entities[self->currentLevel.entityCount].properties->targetX + = calloc(SOKO_MAX_ENTITY_COUNT, sizeof(uint8_t)); + self->currentLevel.entities[self->currentLevel.entityCount].properties->targetX + = calloc(SOKO_MAX_ENTITY_COUNT, sizeof(uint8_t)); + self->currentLevel.entities[self->currentLevel.entityCount].properties->targetCount = 0; + self->currentLevel.entityCount += 1; + i += 3; + break; + case SKB_LASERRECEIVEROMNI: + self->currentLevel.entities[self->currentLevel.entityCount].type = SKE_LASER_RECEIVE_OMNI; + self->currentLevel.entities[self->currentLevel.entityCount].x = objX; + self->currentLevel.entities[self->currentLevel.entityCount].y = objY; + self->currentLevel.entityCount += 1; + i += 2; + break; + case SKB_LASERRECEIVER: + flagByte = self->levelBinaryData[i + 2]; + direction = (flagByte & (0x3 << 6)) >> 6; // flagbyte stores direction in 0bDD0000P0 Where D is + // direction bits and P is player push + players = !!(flagByte & (0x1 < 1)); + + self->currentLevel.entities[self->currentLevel.entityCount].type = SKE_LASER_RECEIVE; + self->currentLevel.entities[self->currentLevel.entityCount].x = objX; + self->currentLevel.entities[self->currentLevel.entityCount].y = objY; + self->currentLevel.entities[self->currentLevel.entityCount].facing = direction; + self->currentLevel.entityCount += 1; + i += 3; + break; + case SKB_LASER90ROTATE: + flagByte = self->levelBinaryData[i + 2]; + direction = !!(flagByte & (0x1 < 0)); + players = !!(flagByte & (0x1 < 1)); + + self->currentLevel.entities[self->currentLevel.entityCount].type = SKE_LASER_90; + self->currentLevel.entities[self->currentLevel.entityCount].x = objX; + self->currentLevel.entities[self->currentLevel.entityCount].y = objY; + self->currentLevel.entities[self->currentLevel.entityCount].facing = direction; + self->currentLevel.entities[self->currentLevel.entityCount].properties + = malloc(sizeof(sokoEntityProperties_t)); + self->currentLevel.entities[self->currentLevel.entityCount].propFlag = true; + self->currentLevel.entities[self->currentLevel.entityCount].properties->players = players; + self->currentLevel.entityCount += 1; + i += 3; + break; + case SKB_GHOSTBLOCK: + flagByte = self->levelBinaryData[i + 2]; + inverted = !!(flagByte & (0x1 < 2)); + players = !!(flagByte & (0x1 < 1)); + + self->currentLevel.entities[self->currentLevel.entityCount].type = SKE_GHOST; + self->currentLevel.entities[self->currentLevel.entityCount].x = objX; + self->currentLevel.entities[self->currentLevel.entityCount].y = objY; + self->currentLevel.entities[self->currentLevel.entityCount].properties + = malloc(sizeof(sokoEntityProperties_t)); + self->currentLevel.entities[self->currentLevel.entityCount].propFlag = true; + self->currentLevel.entities[self->currentLevel.entityCount].properties->players = players; + self->currentLevel.entityCount += 1; + i += 3; + break; + case SKB_OBJEND: + i += 1; + break; + default: // Make the best of an undefined object type and try to skip it by finding its end byte + bool objEndFound = false; + int undefinedObjectLength = 0; + while (!objEndFound) + { + undefinedObjectLength += 1; + if (self->levelBinaryData[i + undefinedObjectLength] == SKB_OBJEND) + { + objEndFound = true; + } + } + i += undefinedObjectLength; // Move to the completion byte of an undefined object type and hope it + // doesn't have two end bytes. + } + } + else + { + int tileX = (tileIndex) % (self->currentLevel.width); + int tileY = (tileIndex) / (self->currentLevel.width); + // self->currentLevel.tiles[tileX][tileY] = self->levelBinaryData[i]; + int tileType = 0; + switch (self->levelBinaryData[i]) // This is a bit easier to read than two arrays + { + case SKB_EMPTY: + tileType = SKT_EMPTY; + break; + case SKB_WALL: + tileType = SKT_WALL; + break; + case SKB_FLOOR: + tileType = SKT_FLOOR; + break; + case SKB_NO_WALK: + tileType = SKT_FLOOR; //@todo Add No-Walk floors that can only accept crates or pass lasers + break; + case SKB_GOAL: + tileType = SKT_GOAL; + self->goals[self->goalCount].x = tileX; + self->goals[self->goalCount].y = tileY; + self->goalCount++; + break; + default: + tileType = SKT_EMPTY; + break; + } + self->currentLevel.tiles[tileX][tileY] = tileType; + // printf("BinData@%d: %d Tile: %d at (%d,%d) + // index:%d\n",i,self->levelBinaryData[i],tileType,tileX,tileY,tileIndex); + tileIndex++; + } + } +} + +static void sokoLoadBinLevel(uint16_t levelIndex) +{ + const int HEADER_BYTE_OFFSET = 2; // Number of bytes before tile data begins + + printf("load level %d\n", levelIndex); + soko->state = SKS_INIT; + size_t fileSize; + soko->levelBinaryData = spiffsReadFile(sokoBinLevelNames[levelIndex], &fileSize, + true); // Heap CAPS malloc/calloc allocation for SPI RAM + // The pointer returned by spiffsReadFile can be freed with free() with no additional steps. + soko->currentLevel.width = soko->levelBinaryData[0]; // first two bytes of a level's data always describe the + // bounding width and height of the tilemap. + soko->currentLevel.height = soko->levelBinaryData[1]; // Max Theoretical Level Bounding Box Size is 255x255, though + // you'll likely run into issues with entities first. + // for(int i = 0; i < fileSize; i++) + //{ + // printf("%d, ",soko->levelBinaryData[i]); + // } + // printf("\n"); + soko->currentLevel.levelScale = 16; + soko->camWidth = TFT_WIDTH / (soko->currentLevel.levelScale); + soko->camHeight = TFT_HEIGHT / (soko->currentLevel.levelScale); + soko->camEnabled = soko->camWidth < soko->currentLevel.width || soko->camHeight < soko->currentLevel.height; + soko->camPadExtentX = soko->camWidth * 0.6 * 0.5; + soko->camPadExtentY = soko->camHeight * 0.6 * 0.5; + + soko->currentLevel.entityCount = 0; + + soko->portalCount = 0; + + sokoLoadBinTiles(soko, (int)fileSize); + // for(int k = 0; k < soko->currentLevel.entityCount; k++) + //{ + // printf("Ent%d:%d + // (%d,%d)",k,soko->currentLevel.entities[k].type,soko->currentLevel.entities[k].x,soko->currentLevel.entities[k].y); + // } + // printf("\n"); +} + +static void sokoLoadLevel(uint16_t levelIndex) +{ + printf("load level %d\n", levelIndex); + soko->state = SKS_INIT; + // get image file from selected index + loadWsg(sokoLevelNames[levelIndex], &soko->levelWSG, false); + + // populate background array + // populate entities array + + soko->currentLevel.width = soko->levelWSG.w; + soko->currentLevel.height = soko->levelWSG.h; + + // player and crate wsg's are 16px right now. + // In picross I wrote a drawWSGScaled for the main screen when i could get away with it on level select screen. but + // here I think just commit to something. + // Maybe 24? How big are levels going to get? + soko->currentLevel.levelScale = 16; + + // how many tiles can fit horizontally and vertically. This doesn't change, and here is we we figure out scale. + // floor + soko->camWidth = TFT_WIDTH / (soko->currentLevel.levelScale); + soko->camHeight = TFT_HEIGHT / (soko->currentLevel.levelScale); + + // enable the camera only if the levelwidth or the levelHeight is greater than camWidth or camHeight. + // should we enable it independently for x/y if the level is thin? + soko->camEnabled = soko->camWidth < soko->currentLevel.width || soko->camHeight < soko->currentLevel.height; + + // percentage of screen to let the player move around in. Small for testing. + // these are half extents. so .7*.5 is 70% of the screen for the movement box. The extent had to be smaller or = + // than half the camsize. + soko->camPadExtentX = soko->camWidth * 0.6 * 0.5; + soko->camPadExtentY = soko->camHeight * 0.6 * 0.5; + + soko->currentLevel.entityCount = 0; + paletteColor_t sampleColor; + soko->portalCount = 0; + + for (size_t x = 0; x < soko->currentLevel.width; x++) + { + for (size_t y = 0; y < soko->currentLevel.height; y++) + { + sampleColor = soko->levelWSG.px[y * soko->levelWSG.w + x]; + soko->currentLevel.tiles[x][y] = sokoGetTileFromColor(sampleColor); + if (soko->currentLevel.tiles[x][y] == SKT_PORTAL) + { + soko->portals[soko->portalCount].index + = soko->portalCount + 1; // For basic test, 1 indexed with levels, but multi-room overworld needs + // more sophistication to keep indeces correct. + soko->portals[soko->portalCount].x = x; + soko->portals[soko->portalCount].y = y; + printf("Portal %d at %d,%d\n", soko->portals[soko->portalCount].index, + soko->portals[soko->portalCount].x, soko->portals[soko->portalCount].y); + soko->portalCount += 1; + } + sokoEntityType_t e = sokoGetEntityFromColor(sampleColor); + if (e != SKE_NONE) + { + soko->currentLevel.entities[soko->currentLevel.entityCount].type = e; + soko->currentLevel.entities[soko->currentLevel.entityCount].x = x; + soko->currentLevel.entities[soko->currentLevel.entityCount].y = y; + if (e == SKE_PLAYER) + { + soko->currentLevel.playerIndex = soko->currentLevel.entityCount; + } + soko->currentLevel.entityCount = soko->currentLevel.entityCount + 1; + } + } + } +} + +static sokoTile_t sokoGetTileFromColor(paletteColor_t col) +{ + // even if player (c005) or crate (c500) is here, they stand on floor. 505 is player and crate, invalid. + if (col == c555 || col == c005 || col == c500 || col == c101) + { + return SKT_FLOOR; + } + else if (col == c000) + { + return SKT_WALL; + } + else if (col == c050 || col == c550 || col == c055) + { // goal is c050, crate and goal is c550, player and goal is c055 + return SKT_GOAL; + } + else if (col == c440) // remember, web safe colors are {0,1,2,3,4,5} cRGB or {0,51,102,153,204,255} decimal. + // Increments of 0x33 or 51. cABC = 0x(0x33*A)(0x33*B)(0x33*C) + { + return SKT_PORTAL; + } + // transparent or invalid is empty. Todo: can catch transparent and report error otherwise... once comitted to + // encoding scheme. + return SKT_EMPTY; +} + +static sokoEntityType_t sokoGetEntityFromColor(paletteColor_t col) +{ + // todo: get actual rgb value from the paletteColors array and check if the rgb values > 0. + if (col == c500 || col == c550) + { + return SKE_CRATE; + } + else if (col == c005 || col == c055) + { // has green. r and b used for entity. g for tile. + return SKE_PLAYER; + } + else if (col == c101) + { + return SKE_STICKY_CRATE; + } + + return SKE_NONE; +} + +// placeholder. +static void sokoBackgroundDrawCallback(int16_t x, int16_t y, int16_t w, int16_t h, int16_t up, int16_t upNum) +{ + // Use TURBO drawing mode to draw individual pixels fast + SETUP_FOR_TURBO(); + + // Draw a grid + for (int16_t yp = y; yp < y + h; yp++) + { + for (int16_t xp = x; xp < x + w; xp++) + { + if ((0 == xp % 20) || (0 == yp % 20)) + { + TURBO_SET_PIXEL(xp, yp, c002); + } + else + { + TURBO_SET_PIXEL(xp, yp, c001); + } + } + } +} + +static int sokoFindIndex(soko_abs_t* self, int targetIndex) +{ + // Filenames are formatted like '1:sk_level.bin:' + int retVal = -1; + for (int i = 0; i < targetIndex; i++) + { + if (self->levelIndeces[i] == targetIndex) + { + retVal = i; + } + } + return retVal; +} + +static void sokoExtractLevelNamesAndIndeces(soko_abs_t* self) +{ + printf("Loading Level List...!\n"); + printf("%s\n", self->levelFileText); + printf("%d\n", (int)strlen(self->levelFileText)); + // char* a = strstr(self->levelFileText,":"); + // char* b = strstr(a,".bin:"); + // printf("%d",(int)((int)b-(int)a)); + // char* stringPtrs[30]; + // memset(stringPtrs,0,30*sizeof(char*)); + char** stringPtrs = soko->levelNames; + memset(stringPtrs, 0, SOKO_LEVEL_COUNT * sizeof(char*)); + int* levelInds = soko->levelIndeces; + memset(levelInds, 0, SOKO_LEVEL_COUNT * sizeof(int)); + int intInd = 0; + int ind = 0; + char* storageStr = strtok(self->levelFileText, ":"); + while (storageStr != NULL) + { + if (strtol(storageStr, NULL, 10) + && !(strstr(storageStr, ".bin"))) // Make sure you're not accidentally reading a number from a filename + { + levelInds[intInd] = (int)strtol(storageStr, NULL, 10); + // printf("NumberThing: %s :: %d\n",storageStr,(int)strtol(storageStr,NULL,10)); + intInd++; + } + else + { + if (!strpbrk(storageStr, "\n\t\r ") && (strstr(storageStr, ".bin"))) + { + int tokLen = strlen(storageStr); + char* tempPtr = calloc((tokLen + 1), sizeof(char)); // Length plus null teminator + // strcpy(tempPtr,storageStr); + // stringPtrs[ind] = tempPtr; + stringPtrs[ind] = storageStr; + // printf("%s\n",storageStr); + ind++; + } + } + // printf("This guy!\n"); + storageStr = strtok(NULL, ":"); + } + printf("Strings: %d, Ints: %d\n", ind, intInd); + printf("Levels and Indeces:\n"); + for (int i = ind - 1; i > -1; i--) + { + printf("Index: %d : %d : %s\n", i, levelInds[i], stringPtrs[i]); + } +} diff --git a/main/modes/soko/soko.h b/main/modes/soko/soko.h new file mode 100644 index 000000000..661e3957a --- /dev/null +++ b/main/modes/soko/soko.h @@ -0,0 +1,267 @@ +#ifndef _SOKO_MODE_H_ +#define _SOKO_MODE_H_ + +#include "swadge2024.h" +#include "soko_input.h" +#include "soko_consts.h" + +extern swadgeMode_t sokoMode; + +typedef enum +{ + SOKO_OVERWORLD = 0, + SOKO_CLASSIC = 1, + SOKO_EULER = 2, + SOKO_LASERBOUNCE = 3 +} soko_var_t; + +typedef enum +{ + SOKO_MENU, + SOKO_LEVELPLAY, + SOKO_LOADNEWLEVEL +} sokoScreen_t; + +typedef enum +{ + SKB_EMPTY = 0, + SKB_WALL = 1, + SKB_FLOOR = 2, + SKB_GOAL = 3, + SKB_NO_WALK = 4, + SKB_OBJSTART = 201, // Object and Signal Bytes are over 200 + SKB_COMPRESS = 202, + SKB_PLAYER = 203, + SKB_CRATE = 204, + SKB_WARPINTERNAL = 205, + SKB_WARPINTERNALEXIT = 206, + SKB_WARPEXTERNAL = 207, + SKB_BUTTON = 208, + SKB_LASEREMITTER = 209, + SKB_LASERRECEIVEROMNI = 210, + SKB_LASERRECEIVER = 211, + SKB_LASER90ROTATE = 212, + SKB_GHOSTBLOCK = 213, + SKB_OBJEND = 230 +} soko_bin_t; // Binary file byte value decode list +typedef struct soko_portal_s +{ + uint8_t x; + uint8_t y; + uint8_t index; + bool levelCompleted; // use this to show completed levels later +} soko_portal_t; + +typedef struct soko_goal_s +{ + uint8_t x; + uint8_t y; +} soko_goal_t; + +typedef enum +{ + SKS_INIT, ///< meta enum used for edge cases + SKS_GAMEPLAY, + SKS_VICTORY, +} sokoGameState_t; + +/* +typedef enum +{ + SOKO_OVERWORLD = 0, + SOKO_CLASSIC = 1, + SOKO_EULER = 2 +} soko_var_t; +*/ + +typedef enum +{ + SKE_NONE = 0, + SKE_PLAYER = 1, + SKE_CRATE = 2, + SKE_LASER_90 = 3, + SKE_STICKY_CRATE = 4, + SKE_WARP = 5, + SKE_BUTTON = 6, + SKE_LASER_EMIT_UP = 7, + SKE_LASER_RECEIVE_OMNI = 8, + SKE_LASER_RECEIVE = 9, + SKE_GHOST = 10 +} sokoEntityType_t; + +typedef enum +{ + SKT_EMPTY = 0, + SKT_FLOOR = 1, + SKT_WALL = 2, + SKT_GOAL = 3, + SKT_PORTAL = 4, + SKT_LASER_EMIT = 5, // To Be Removed + SKT_LASER_RECEIVE = 6, // To Be Removed + SKT_FLOOR_WALKED = 7, + SKT_NO_WALK = 8 +} sokoTile_t; + +typedef struct +{ + bool sticky; // For Crates, this determines if crates stick to players. For Buttons, this determines if the button + // stays down. + bool trail; // Crates leave Euler trails + bool players; // For Crates, allow player push. For Button, allow player press. + bool crates; // For Buttons, allow crate push. For Portals, allow crate transport. + bool inverted; // For Buttons, invert default state of affected blocks. For ghost blocks, inverts default + // tangibility. Button and Ghostblock with both cancel. + uint8_t* targetX; + uint8_t* targetY; + uint8_t targetCount; + uint8_t hp; +} sokoEntityProperties_t; // this is a separate type so that it can be allocated as several different types with a void + // pointer and some aggressive casting. + +typedef struct +{ + sokoEntityType_t type; + uint16_t x; + uint16_t y; + sokoDirection_t facing; + sokoEntityProperties_t* properties; + bool propFlag; +} sokoEntity_t; + +typedef struct sokoVec_s +{ + int16_t x; + int16_t y; +} sokoVec_t; + +typedef struct sokoCollision_s +{ + uint16_t x; + uint16_t y; + uint16_t entityFlag; + uint16_t entityIndex; + +} sokoCollision_t; + +typedef struct +{ + wsg_t playerWSG; + wsg_t playerUpWSG; + wsg_t playerRightWSG; + wsg_t playerLeftWSG; + wsg_t playerDownWSG; + wsg_t crateWSG; + wsg_t stickyCrateWSG; + paletteColor_t wallColor; + paletteColor_t floorColor; + +} sokoTheme_t; + +typedef struct +{ + uint16_t levelScale; + uint8_t width; + uint8_t height; + uint8_t entityCount; + uint16_t playerIndex; // we could have multiple players... + sokoTile_t tiles[SOKO_MAX_LEVELSIZE][SOKO_MAX_LEVELSIZE]; + sokoEntity_t entities[SOKO_MAX_ENTITY_COUNT]; // todo: pointer and runtime array size + soko_var_t gameMode; +} sokoLevel_t; + +typedef struct +{ + // meta + menu_t* menu; ///< The menu structure + menuLogbookRenderer_t* menuLogbookRenderer; ///< Renderer for the menu + font_t ibm; ///< The font used in the menu and game + sokoScreen_t screen; ///< The screen being displayed + + // game settings + uint16_t maxPush; ///< Maximum number of crates the player can push. Use 0 for no limit. + sokoGameState_t state; + + // level + char* levels[SOKO_LEVEL_COUNT]; ///< List of wsg filenames. not comitted to storing level data like this, but idk if + ///< I need level names like picross. + wsg_t levelWSG; ///< Current level + + // input + sokoGameplayInput_t input; + + // current level + sokoLevel_t currentLevel; + bool allCratesOnGoal; + +} soko_t; + +typedef struct soko_abs_s soko_abs_t; +typedef struct soko_abs_s +{ + // meta + menu_t* menu; ///< The menu structure + menuLogbookRenderer_t* menuLogbookRenderer; ///< Renderer for the menu + font_t ibm; ///< The font used in the menu and game + sokoScreen_t screen; ///< The screen being displayed + + char* levelFileText; + char* levelNames[SOKO_LEVEL_COUNT]; + int levelIndeces[SOKO_LEVEL_COUNT]; + + // game settings + uint16_t maxPush; ///< Maximum number of crates the player can push. Use 0 for no limit. + sokoGameState_t state; + + // theme settings + sokoTheme_t* currentTheme; ///< Points to one of the other themes. + sokoTheme_t overworldTheme; + sokoTheme_t sokoDefaultTheme; + + // level + char* levels[SOKO_LEVEL_COUNT]; ///< List of wsg filenames. not comitted to storing level data like this, but idk if + ///< I need level names like picross. + wsg_t levelWSG; ///< Current level + uint8_t* levelBinaryData; + + soko_portal_t portals[SOKO_MAX_PORTALS]; + uint8_t portalCount; + + soko_goal_t goals[SOKO_MAX_GOALS]; + uint8_t goalCount; + + // input + sokoGameplayInput_t input; + + // current level + sokoLevel_t currentLevel; + bool allCratesOnGoal; + + // camera features + bool camEnabled; + uint16_t camX; + uint16_t camY; + uint16_t camPadExtentX; + uint16_t camPadExtentY; + uint16_t camWidth; + uint16_t camHeight; + + // game loop functions //Functions are moved into game struct so engine can support different game rules + void (*gameLoopFunc)(soko_abs_t* self, int64_t elapsedUs); + void (*sokoTryPlayerMovementFunc)(soko_abs_t* self); + bool (*sokoTryMoveEntityInDirectionFunc)(soko_abs_t* self, sokoEntity_t* entity, int dx, int dy, uint16_t push); + void (*drawTilesFunc)(soko_abs_t* self, sokoLevel_t* level); + bool (*isVictoryConditionFunc)(soko_abs_t* self); + sokoTile_t (*sokoGetTileFunc)(soko_abs_t* self, int x, int y); + + // Player Convenience Pointer + sokoEntity_t* soko_player; + + bool loadNewLevelFlag; + uint8_t loadNewLevelIndex; + soko_var_t loadNewLevelVariant; + +} soko_abs_t; + +// soko_t* soko; + +#endif \ No newline at end of file diff --git a/main/modes/soko/soko_consts.h b/main/modes/soko/soko_consts.h new file mode 100644 index 000000000..f4516acc3 --- /dev/null +++ b/main/modes/soko/soko_consts.h @@ -0,0 +1,12 @@ +#ifndef SOKO_CONSTS_H +#define SOKO_CONSTS_H + +#define SOKO_LEVEL_COUNT 30 +#define SOKO_MAX_LEVELSIZE 30 +#define SOKO_MAX_ENTITY_COUNT 15 +#define SOKO_MAX_PORTALS 10 +#define SOKO_MAX_GOALS 20 +#define SOKO_MAX_REDIRECTS 15 // Should be equal to MAX_ENTITY until I find an edge case +#define SOKO_VICTORY_TIMER_US 1000000 + +#endif // SOKO_CONSTS_H \ No newline at end of file diff --git a/main/modes/soko/soko_game.c b/main/modes/soko/soko_game.c new file mode 100644 index 000000000..226c81840 --- /dev/null +++ b/main/modes/soko/soko_game.c @@ -0,0 +1,313 @@ +#include "soko_game.h" +#include "soko.h" +#include "soko_gamerules.h" + +/* +void sokoTryPlayerMovement(void); +sokoTile_t sokoGetTile(int, int); +bool sokoTryMoveEntityInDirection(sokoEntity_t*, int, int,uint16_t); +bool allCratesOnGoal(void); + +*/ +// sokoDirection_t sokoDirectionFromDelta(int, int); + +// soko_t* s; +// sokoEntity_t* player; + +soko_abs_t* soko_s; + +void sokoInitGameBin(soko_abs_t* soko) +{ + printf("init sokoban game binary"); + + soko_s = soko; + soko_s->soko_player = &soko_s->currentLevel.entities[soko_s->currentLevel.playerIndex]; + + soko->camX = soko_s->soko_player->x; + soko->camY = soko_s->soko_player->y; + + sokoInitInput(&soko_s->input); + + soko->state = SKS_GAMEPLAY; + + sokoConfigGamemode(soko, soko->currentLevel.gameMode); +} + +void sokoInitGame(soko_abs_t* soko) +{ + printf("init sokobon game.\n"); + + // Configure conveninence pointers. + soko_s = soko; + soko_s->soko_player = &soko_s->currentLevel.entities[soko_s->currentLevel.playerIndex]; + + // reset camera + soko->camX = soko_s->soko_player->x; + soko->camY = soko_s->soko_player->y; + + sokoInitInput(&soko_s->input); + + soko->state = SKS_GAMEPLAY; + + sokoConfigGamemode(soko, SOKO_OVERWORLD); + + // sokoConfigGamemode(soko,SOKO_EULER); +} + +void sokoInitNewLevel(soko_abs_t* soko, soko_var_t variant) +{ + printf("Init New Level.\n"); + + soko_s = soko; + soko_s->soko_player = &soko_s->currentLevel.entities[soko_s->currentLevel.playerIndex]; + sokoInitInput(&soko_s->input); + + // set gameplay settings from default settings, if we want powerups or whatever that adjusts them, or have a state + // machine. + soko_s->maxPush = 0; // set to 1 for "traditional" sokoban. + + soko->state = SKS_GAMEPLAY; + + sokoConfigGamemode(soko, variant); +} + +/* +void gameLoop(int64_t elapsedUs) +{ + if(s->state == SKS_GAMEPLAY) + { + //logic + sokoTryPlayerMovement(); + + //victory status. stored separate from gamestate because of future gameplay ideas/remixes. + s->allCratesOnGoal = allCratesOnGoal(); + if(s->allCratesOnGoal){ + s->state = SKS_VICTORY; + } + //draw level + drawTiles(&s->currentLevel); + + }else if(s->state == SKS_VICTORY) + { + //check for input for exit/next level. + drawTiles(&s->currentLevel); + } + + + //DEBUG PLACEHOLDER: + // Render the time to a string + char str[16] = {0}; + int16_t tWidth; + if(!s->allCratesOnGoal) + { + snprintf(str, sizeof(str) - 1, "sokoban"); + // Measure the width of the time string + tWidth = textWidth(&s->ibm, str); + // Draw the time string to the display, centered at (TFT_WIDTH / 2) + drawText(&s->ibm, c555, str, ((TFT_WIDTH - tWidth) / 2), 0); + }else + { + snprintf(str, sizeof(str) - 1, "sokasuccess"); + // Measure the width of the time string + tWidth = textWidth(&s->ibm, str); + // Draw the time string to the display, centered at (TFT_WIDTH / 2) + drawText(&s->ibm, c555, str, ((TFT_WIDTH - tWidth) / 2), 0); + } + +} + + +//Gameplay Logic +void sokoTryPlayerMovement() +{ + + if(s->input.playerInputDeltaX == 0 && s->input.playerInputDeltaY == 0) + { + return; + } + + sokoTryMoveEntityInDirection(player,s->input.playerInputDeltaX,s->input.playerInputDeltaY,0); +} + + +bool sokoTryMoveEntityInDirection(sokoEntity_t* entity, int dx, int dy, uint16_t push) +{ + //prevent infitnite loop where you push yourself nowhere. + if(dx == 0 && dy == 0 ) + { + return false; + } + + //maxiumum number of crates we can push. Traditional sokoban has a limit of one. I prefer infinite for challenges. + if(s->maxPush != 0 && push>s->maxPush) + { + return false; + } + + int px = entity->x+dx; + int py = entity->y+dy; + sokoTile_t nextTile = sokoGetTile(px,py); + + if(nextTile == SKT_FLOOR || nextTile == SKT_GOAL || nextTile == SKT_EMPTY) + { + //Is there an entity at this position? + for (size_t i = 0; i < s->currentLevel.entityCount; i++) + { + //is pushable. + if(s->currentLevel.entities[i].type == SKE_CRATE) + { + if(s->currentLevel.entities[i].x == px && s->currentLevel.entities[i].y == py) + { + if(sokoTryMoveEntityInDirection(&s->currentLevel.entities[i],dx,dy,push+1)) + { + entity->x += dx; + entity->y += dy; + entity->facing = sokoDirectionFromDelta(dx,dy); + return true; + }else{ + //can't push? can't move. + return false; + } + + } + } + + } + + //No wall in front of us and nothing to push, we can move. + entity->x += dx; + entity->y += dy; + entity->facing = sokoDirectionFromDelta(dx,dy); + return true; + } + + return false; +} + +//draw the tiles (and entities, for now) of the level. +void drawTiles(sokoLevel_t* level) +{ + SETUP_FOR_TURBO(); + uint16_t scale = level->levelScale; + uint16_t ox = (TFT_WIDTH/2)-((level->width)*scale/2); + uint16_t oy = (TFT_HEIGHT/2)-((level->height)*scale/2); + + for (size_t x = 0; x < level->width; x++) + { + for (size_t y = 0; y < level->height; y++) + { + paletteColor_t color = cTransparent; + switch (level->tiles[x][y]) + { + case SKT_FLOOR: + color = c444; + break; + case SKT_WALL: + color = c111; + break; + case SKT_GOAL: + color = c141; + break; + case SKT_EMPTY: + color = cTransparent; + default: + break; + } + + //Draw a square. + //none of this matters it's all getting replaced with drawwsg later. + if(color != cTransparent){ + for (size_t xd = ox+x*scale; xd < ox+x*scale+scale; xd++) + { + for (size_t yd = oy+y*scale; yd < oy+y*scale+scale; yd++) + { + TURBO_SET_PIXEL(xd, yd, color); + } + } + } + //draw outline around the square. + //drawRect(ox+x*s,oy+y*s,ox+x*s+s,oy+y*s+s,color); + } + } + + for (size_t i = 0; i < level->entityCount; i++) + { + switch (level->entities[i].type) + { + case SKE_PLAYER: + switch(level->entities[i].facing){ + case SKD_UP: + drawWsg(&s->playerUpWSG,ox+level->entities[i].x*scale,oy+level->entities[i].y*scale,false,false,0); + break; + case SKD_RIGHT: + drawWsg(&s->playerRightWSG,ox+level->entities[i].x*scale,oy+level->entities[i].y*scale,false,false,0); + break; + case SKD_LEFT: + drawWsg(&s->playerLeftWSG,ox+level->entities[i].x*scale,oy+level->entities[i].y*scale,false,false,0); + break; + case SKD_DOWN: + default: + drawWsg(&s->playerDownWSG,ox+level->entities[i].x*scale,oy+level->entities[i].y*scale,false,false,0); + break; + } + + break; + case SKE_CRATE: + drawWsg(&s->crateWSG,ox+level->entities[i].x*scale,oy+level->entities[i].y*scale,false,false,0); + case SKE_NONE: + default: + break; + } + } + +} + +bool allCratesOnGoal() +{ + for (size_t i = 0; i < s->currentLevel.entityCount; i++) + { + if(s->currentLevel.entities[i].type == SKE_CRATE) + { + if(s->currentLevel.tiles[s->currentLevel.entities[i].x][s->currentLevel.entities[i].y] != SKT_GOAL) + { + return false; + } + } + } + + return true; +} + + +sokoDirection_t sokoDirectionFromDelta(int dx,int dy) +{ + if(dx > 0 && dy == 0) + { + return SKD_RIGHT; + }else if(dx < 0 && dy == 0) + { + return SKD_LEFT; + }else if(dx == 0 && dy < 0) + { + return SKD_UP; + }else if(dx == 0 && dy > 0) + { + return SKD_DOWN; + } + + return SKD_NONE; +} +sokoTile_t sokoGetTile(int x, int y) +{ + if(x<0 || x >= s->currentLevel.width) + { + return SKT_WALL; + } + if(y<0 || y >= s->currentLevel.height) + { + return SKT_WALL; + } + + return s->currentLevel.tiles[x][y]; +} +*/ \ No newline at end of file diff --git a/main/modes/soko/soko_game.h b/main/modes/soko/soko_game.h new file mode 100644 index 000000000..41c60aaf0 --- /dev/null +++ b/main/modes/soko/soko_game.h @@ -0,0 +1,12 @@ +#ifndef SOKO_GAME_H +#define SOKO_GAME_H + +#include "soko.h" + +void sokoInitGame(soko_abs_t*); +void sokoInitGameBin(soko_abs_t*); +void sokoInitNewLevel(soko_abs_t* soko, soko_var_t variant); +void gameLoop(int64_t); +void drawTiles(sokoLevel_t*); + +#endif // SOKO_GAME_H \ No newline at end of file diff --git a/main/modes/soko/soko_gamerules.c b/main/modes/soko/soko_gamerules.c new file mode 100644 index 000000000..8abdb2d3f --- /dev/null +++ b/main/modes/soko/soko_gamerules.c @@ -0,0 +1,1069 @@ +#include "soko_game.h" +#include "soko.h" +#include "soko_gamerules.h" + +#include "shapes.h" + +// clang-format off +// True if the entity CANNOT go on the tile +bool sokoEntityTileCollision[5][8] = { + // Empty, //floor //wall //goal //portal //l-emit //l-receive //walked + {true, false, true, false, false, false, false, false}, // SKE_NONE + {true, false, true, false, false, false, false, true}, // PLAYER + {true, false, true, false, false, false, false, false}, // CRATE + {true, false, true, false, false, false, false, false}, // LASER + {true, false, true, false, false, false, false, false}, // STICKY CRATE +}; +// clang-format on + +uint64_t victoryDanceTimer; + +void sokoConfigGamemode( + soko_abs_t* gamestate, + soko_var_t variant) // This should be called when you reload a level to make sure game rules are correct +{ + gamestate->currentTheme = &gamestate->sokoDefaultTheme; + if (variant == SOKO_CLASSIC) // standard gamemode. Check 'variant' variable + { + printf("Config Soko to Classic\n"); + gamestate->maxPush = 1; // set to 1 for "traditional" sokoban. + gamestate->gameLoopFunc = absSokoGameLoop; + gamestate->sokoTryPlayerMovementFunc = absSokoTryPlayerMovement; + gamestate->sokoTryMoveEntityInDirectionFunc = absSokoTryMoveEntityInDirection; + gamestate->drawTilesFunc = absSokoDrawTiles; + gamestate->isVictoryConditionFunc = absSokoAllCratesOnGoal; + gamestate->sokoGetTileFunc = absSokoGetTile; + } + else if (variant == SOKO_EULER) // standard gamemode. Check 'variant' variable + { + printf("Config Soko to Euler\n"); + gamestate->maxPush = 0; // set to 0 for infinite push. + gamestate->gameLoopFunc = absSokoGameLoop; + gamestate->sokoTryPlayerMovementFunc = eulerSokoTryPlayerMovement; + gamestate->sokoTryMoveEntityInDirectionFunc = absSokoTryMoveEntityInDirection; + gamestate->drawTilesFunc = absSokoDrawTiles; + gamestate->isVictoryConditionFunc = eulerNoUnwalkedFloors; + gamestate->sokoGetTileFunc = absSokoGetTile; + } + else if (variant == SOKO_OVERWORLD) + { + printf("Config Soko to Overworld\n"); + gamestate->maxPush = 0; // set to 0 for infinite push. + gamestate->gameLoopFunc = overworldSokoGameLoop; + gamestate->sokoTryPlayerMovementFunc = absSokoTryPlayerMovement; + gamestate->sokoTryMoveEntityInDirectionFunc = absSokoTryMoveEntityInDirection; + gamestate->drawTilesFunc = absSokoDrawTiles; + gamestate->isVictoryConditionFunc = overworldPortalEntered; + gamestate->sokoGetTileFunc = absSokoGetTile; + + gamestate->currentTheme = &gamestate->overworldTheme; + } + else if (variant == SOKO_LASERBOUNCE) + { + printf("Config Soko to Laser Bounce\n"); + gamestate->maxPush = 0; // set to 0 for infinite push. + gamestate->gameLoopFunc = laserBounceSokoGameLoop; + gamestate->sokoTryPlayerMovementFunc = absSokoTryPlayerMovement; + gamestate->sokoTryMoveEntityInDirectionFunc = absSokoTryMoveEntityInDirection; + gamestate->drawTilesFunc = absSokoDrawTiles; + gamestate->isVictoryConditionFunc = absSokoAllCratesOnGoal; + gamestate->sokoGetTileFunc = absSokoGetTile; + } + + // add conditional for alternative variants +} + +void laserBounceSokoGameLoop(soko_abs_t* self, int64_t elapsedUs) +{ + if (self->state == SKS_GAMEPLAY) + { + // logic + self->sokoTryPlayerMovementFunc(self); + + // victory status. stored separate from gamestate because of future gameplay ideas/remixes. + // todo: rename to isVictory or such. + self->allCratesOnGoal = self->isVictoryConditionFunc(self); + if (self->allCratesOnGoal) + { + self->state = SKS_VICTORY; + victoryDanceTimer = 0; + } + // draw level + self->drawTilesFunc(self, &self->currentLevel); + drawLaserFromEntity(self, self->soko_player); + } + else if (self->state == SKS_VICTORY) + { + // check for input for exit/next level. + self->drawTilesFunc(self, &self->currentLevel); + victoryDanceTimer += elapsedUs; + if (victoryDanceTimer > SOKO_VICTORY_TIMER_US) + { + self->loadNewLevelIndex = 0; + self->loadNewLevelFlag = true; + self->screen = SOKO_LOADNEWLEVEL; + } + } + + // DEBUG PLACEHOLDER: + // Render the time to a string + char str[16] = {0}; + int16_t tWidth; + if (!self->allCratesOnGoal) + { + snprintf(str, sizeof(str) - 1, "sokoban"); + // Measure the width of the time string + tWidth = textWidth(&self->ibm, str); + // Draw the time string to the display, centered at (TFT_WIDTH / 2) + drawText(&self->ibm, c555, str, ((TFT_WIDTH - tWidth) / 2), 0); + } + else + { + snprintf(str, sizeof(str) - 1, "sokasuccess"); + // Measure the width of the time string + tWidth = textWidth(&self->ibm, str); + // Draw the time string to the display, centered at (TFT_WIDTH / 2) + drawText(&self->ibm, c555, str, ((TFT_WIDTH - tWidth) / 2), 0); + } + sharedGameLoop(self); +} + +void absSokoGameLoop(soko_abs_t* self, int64_t elapsedUs) +{ + if (self->state == SKS_GAMEPLAY) + { + // logic + self->sokoTryPlayerMovementFunc(self); + + // victory status. stored separate from gamestate because of future gameplay ideas/remixes. + // todo: rename to isVictory or such. + self->allCratesOnGoal = self->isVictoryConditionFunc(self); + if (self->allCratesOnGoal) + { + self->state = SKS_VICTORY; + victoryDanceTimer = 0; + } + // draw level + self->drawTilesFunc(self, &self->currentLevel); + } + else if (self->state == SKS_VICTORY) + { + // check for input for exit/next level. + self->drawTilesFunc(self, &self->currentLevel); + victoryDanceTimer += elapsedUs; + if (victoryDanceTimer > SOKO_VICTORY_TIMER_US) + { + self->loadNewLevelIndex = 0; + self->loadNewLevelFlag = true; + self->screen = SOKO_LOADNEWLEVEL; + } + } + + // DEBUG PLACEHOLDER: + char str[16] = {0}; + int16_t tWidth; + if (!self->allCratesOnGoal) + { + snprintf(str, sizeof(str) - 1, "sokoban"); + // Measure the width of the time string + tWidth = textWidth(&self->ibm, str); + // Draw the time string to the display, centered at (TFT_WIDTH / 2) + drawText(&self->ibm, c555, str, ((TFT_WIDTH - tWidth) / 2), 0); + } + else + { + snprintf(str, sizeof(str) - 1, "sokasuccess"); + // Measure the width of the time string + tWidth = textWidth(&self->ibm, str); + // Draw the time string to the display, centered at (TFT_WIDTH / 2) + drawText(&self->ibm, c555, str, ((TFT_WIDTH - tWidth) / 2), 0); + } + sharedGameLoop(self); +} + +void sharedGameLoop(soko_abs_t* self) +{ + if (self->input.restartLevel) + { + restartCurrentLevel(self); + } + else if (self->input.exitToOverworld) + { + exitToOverworld(self); + } +} + +// Gameplay Logic +void absSokoTryPlayerMovement(soko_abs_t* self) +{ + if (self->input.playerInputDeltaX == 0 && self->input.playerInputDeltaY == 0) + { + return; + } + + self->sokoTryMoveEntityInDirectionFunc(self, self->soko_player, self->input.playerInputDeltaX, + self->input.playerInputDeltaY, 0); +} + +bool absSokoTryMoveEntityInDirection(soko_abs_t* self, sokoEntity_t* entity, int dx, int dy, uint16_t push) +{ + // prevent infitnite loop where you push yourself nowhere. + if (dx == 0 && dy == 0) + { + return false; + } + + // maxiumum number of crates we can push. Traditional sokoban has a limit of one. I prefer infinite for challenges. + if (self->maxPush != 0 && push > self->maxPush) + { + return false; + } + + int px = entity->x + dx; + int py = entity->y + dy; + sokoTile_t nextTile = self->sokoGetTileFunc(self, px, py); + + // when this is false, we CAN move. True for Collision. + if (!sokoEntityTileCollision[entity->type][nextTile]) + { + // Is there an entity at this position? + for (size_t i = 0; i < self->currentLevel.entityCount; i++) + { + // is pushable. + if (self->currentLevel.entities[i].type == SKE_CRATE + || self->currentLevel.entities[i].type == SKE_STICKY_CRATE) + { + if (self->currentLevel.entities[i].x == px && self->currentLevel.entities[i].y == py) + { + if (self->sokoTryMoveEntityInDirectionFunc(self, &self->currentLevel.entities[i], dx, dy, push + 1)) + { + printf("pushing entity"); + entity->x += dx; + entity->y += dy; + entity->facing = sokoDirectionFromDelta(dx, dy); + return true; // if entities overlap, we should not break here? + } + else + { + // can't push? can't move. + return false; + } + } + } + } + + // No wall in front of us and nothing to push, we can move. + entity->x += dx; + entity->y += dy; + entity->facing = sokoDirectionFromDelta(dx, dy); + return true; + } // all other floor types invalid. Be careful when we add tile types in different rule sets. + + return false; +} + +// draw the tiles (and entities, for now) of the level. +void absSokoDrawTiles(soko_abs_t* self, sokoLevel_t* level) +{ + uint16_t scale = level->levelScale; + // These are in level space (not pixels) and must be within bounds of currentLevel.tiles. + int16_t screenMinX, screenMaxX, screenMinY, screenMaxY; + // offsets. + uint16_t ox, oy; + + // Recalculate Camera Position + // todo: extract to a function if we end up with different draw functions. Part of future pointer refactor. + if (self->camEnabled) + { + // calculate camera position. Shift if needed. Cam position was initiated to player position. + if (self->soko_player->x > self->camX + self->camPadExtentX) + { + self->camX = self->soko_player->x - self->camPadExtentX; + } + else if (self->soko_player->x < self->camX - self->camPadExtentX) + { + self->camX = self->soko_player->x + self->camPadExtentX; + } + else if (self->soko_player->y > self->camY + self->camPadExtentY) + { + self->camY = self->soko_player->y - self->camPadExtentY; + } + else if (self->soko_player->y < self->camY - self->camPadExtentY) + { + self->camY = self->soko_player->y + self->camPadExtentY; + } + + // calculate offsets + ox = -self->camX * scale + (TFT_WIDTH / 2); + oy = -self->camY * scale + (TFT_HEIGHT / 2); + + // calculate out of bounds draws. todo: make tenery operators. + screenMinX = self->camX - self->camWidth / 2 - 1; + if (screenMinX < 0) + { + screenMinX = 0; + } + screenMaxX = self->camX + self->camWidth / 2 + 1; + if (screenMaxX > level->width) + { + screenMaxX = level->width; + } + screenMinY = self->camY - self->camHeight / 2 - 1; + if (screenMinY < 0) + { + screenMinY = 0; + } + screenMaxY = self->camY + self->camHeight / 2 + 1; + if (screenMaxY > level->height) + { + screenMaxY = level->height; + } + } + else + { // no camera + // calculate offsets to center the level. + ox = (TFT_WIDTH / 2) - ((level->width) * scale / 2); + oy = (TFT_HEIGHT / 2) - ((level->height) * scale / 2); + + // bounds are just the level. + screenMinX = 0; + screenMaxX = level->width; + screenMinY = 0; + screenMaxY = level->height; + } + + SETUP_FOR_TURBO(); + + // uint16_t DEBUG_DRAW_COUNT=0; + + for (size_t x = screenMinX; x < screenMaxX; x++) + { + for (size_t y = screenMinY; y < screenMaxY; y++) + { + paletteColor_t color = cTransparent; + switch (level->tiles[x][y]) + { + case SKT_FLOOR: + color = self->currentTheme->floorColor; + break; + case SKT_WALL: + color = self->currentTheme->wallColor; + break; + case SKT_GOAL: + color = c141; + break; + case SKT_FLOOR_WALKED: + color = c334; + break; + case SKT_EMPTY: + color = cTransparent; + break; + case SKT_PORTAL: + color = c440; + break; + default: + break; + } + + // Draw a square. + // none of this matters it's all getting replaced with drawwsg later. + if (color != cTransparent) + { + for (size_t xd = ox + x * scale; xd < ox + x * scale + scale; xd++) + { + for (size_t yd = oy + y * scale; yd < oy + y * scale + scale; yd++) + { + TURBO_SET_PIXEL(xd, yd, color); + } + } + } + // DEBUG_DRAW_COUNT++; + // draw outline around the square. + // drawRect(ox+x*s,oy+y*s,ox+x*s+s,oy+y*s+s,color); + } + } + + for (size_t i = 0; i < level->entityCount; i++) + { + // don't bother drawing off screen + if (level->entities[i].x >= screenMinX && level->entities[i].x <= screenMaxX + && level->entities[i].y >= screenMinY && level->entities[i].y <= screenMaxY) + { + switch (level->entities[i].type) + { + case SKE_PLAYER: + switch (level->entities[i].facing) + { + case SKD_UP: + drawWsg(&self->currentTheme->playerUpWSG, ox + level->entities[i].x * scale, + oy + level->entities[i].y * scale, false, false, 0); + break; + case SKD_RIGHT: + drawWsg(&self->currentTheme->playerRightWSG, ox + level->entities[i].x * scale, + oy + level->entities[i].y * scale, false, false, 0); + break; + case SKD_LEFT: + drawWsg(&self->currentTheme->playerLeftWSG, ox + level->entities[i].x * scale, + oy + level->entities[i].y * scale, false, false, 0); + break; + case SKD_DOWN: + default: + drawWsg(&self->currentTheme->playerDownWSG, ox + level->entities[i].x * scale, + oy + level->entities[i].y * scale, false, false, 0); + break; + } + + break; + case SKE_CRATE: + drawWsg(&self->currentTheme->crateWSG, ox + level->entities[i].x * scale, + oy + level->entities[i].y * scale, false, false, 0); + break; + case SKE_STICKY_CRATE: + drawWsg(&self->currentTheme->stickyCrateWSG, ox + level->entities[i].x * scale, + oy + level->entities[i].y * scale, false, false, 0); + break; + case SKE_NONE: + default: + break; + } + // DEBUG_DRAW_COUNT++; + } + } +} + +/* +//draw the tiles (and entities, for now) of the level. +void absSokoDrawTiles(soko_abs_t *self, sokoLevel_t* level) +{ + SETUP_FOR_TURBO(); + uint16_t scale = level->levelScale; + uint16_t ox = (TFT_WIDTH/2)-((level->width)*scale/2); + uint16_t oy = (TFT_HEIGHT/2)-((level->height)*scale/2); + + for (size_t x = 0; x < level->width; x++) + { + for (size_t y = 0; y < level->height; y++) + { + paletteColor_t color = cTransparent; + switch (level->tiles[x][y]) + { + case SKT_FLOOR: + color = c444; + break; + case SKT_WALL: + color = c111; + break; + case SKT_GOAL: + color = c141; + break; + case SKT_EMPTY: + color = cTransparent; + default: + break; + } + + //Draw a square. + //none of this matters it's all getting replaced with drawwsg later. + if(color != cTransparent){ + for (size_t xd = ox+x*scale; xd < ox+x*scale+scale; xd++) + { + for (size_t yd = oy+y*scale; yd < oy+y*scale+scale; yd++) + { + TURBO_SET_PIXEL(xd, yd, color); + } + } + } + //draw outline around the square. + //drawRect(ox+x*s,oy+y*s,ox+x*s+s,oy+y*s+s,color); + } + } + + for (size_t i = 0; i < level->entityCount; i++) + { + switch (level->entities[i].type) + { + case SKE_PLAYER: + // +drawCircleFilled(ox+level->entities[i].x*scale+scale/2,oy+level->entities[i].y*scale+scale/2,scale/2-1,c411); + drawWsg(&self->playerWSG,ox+level->entities[i].x*scale,oy+level->entities[i].y*scale,false,false,0); + break; + case SKE_CRATE: + //drawCircleFilled(ox+level->entities[i].x*scale+scale/2,oy+level->entities[i].y*scale+scale/2,scale/2-1,c441); + drawWsg(&self->crateWSG,ox+level->entities[i].x*scale,oy+level->entities[i].y*scale,false,false,0); + case SKE_NONE: + default: + break; + } + } + +} +*/ +bool absSokoAllCratesOnGoal(soko_abs_t* self) +{ + // printf("Victory Enter\n"); + for (size_t i = 0; i < self->currentLevel.entityCount; i++) + { + // printf("Loop Enter "); + if (self->currentLevel.entities[i].type == SKE_CRATE) + { + // printf("Crate Found "); + if (self->currentLevel.tiles[self->currentLevel.entities[i].x][self->currentLevel.entities[i].y] + != SKT_GOAL) + { + // printf("Crate Off Goal"); + return false; + } + } + // printf("\n"); + } + // printf("Victory True\n"); + return true; +} + +sokoTile_t absSokoGetTile(soko_abs_t* self, int x, int y) +{ + if (x < 0 || x >= self->currentLevel.width) + { + return SKT_WALL; + } + if (y < 0 || y >= self->currentLevel.height) + { + return SKT_WALL; + } + + return self->currentLevel.tiles[x][y]; +} + +sokoDirection_t sokoDirectionFromDelta(int dx, int dy) +{ + if (dx > 0 && dy == 0) + { + return SKD_RIGHT; + } + else if (dx < 0 && dy == 0) + { + return SKD_LEFT; + } + else if (dx == 0 && dy < 0) + { + return SKD_UP; + } + else if (dx == 0 && dy > 0) + { + return SKD_DOWN; + } + + return SKD_NONE; +} + +sokoVec_t sokoGridToPix(soko_abs_t* self, sokoVec_t grid) // Convert grid position to screen pixel position +{ + sokoVec_t retVec; + SETUP_FOR_TURBO(); + uint16_t scale + = self->currentLevel + .levelScale; //@todo These should be in constants, but too lazy to change all references at the moment. + uint16_t ox = (TFT_WIDTH / 2) - ((self->currentLevel.width) * scale / 2); + uint16_t oy = (TFT_HEIGHT / 2) - ((self->currentLevel.height) * scale / 2); + retVec.x = ox + scale * grid.x + scale / 2; + retVec.y = oy + scale * grid.y + scale / 2; + return retVec; +} + +void drawLaserFromEntity(soko_abs_t* self, sokoEntity_t* emitter) +{ + sokoCollision_t impactSpot = sokoBeamImpact(self, self->soko_player); + // printf("Player Pos: x:%d,y:%d Facing:%d Impact Result: x:%d,y:%d, Flag:%d + // Index:%d\n",self->soko_player->x,self->soko_player->y,self->soko_player->facing,impactSpot.x,impactSpot.y,impactSpot.entityFlag,impactSpot.entityIndex); + sokoVec_t playerGrid, impactGrid; + playerGrid.x = emitter->x; + playerGrid.y = emitter->y; + impactGrid.x = impactSpot.x; + impactGrid.y = impactSpot.y; + sokoVec_t playerPix = sokoGridToPix(self, playerGrid); + sokoVec_t impactPix = sokoGridToPix(self, impactGrid); + drawLine(playerPix.x, playerPix.y, impactPix.x, impactPix.y, c500, 0); +} + +int sokoBeamImpactRecursive(soko_abs_t* self, int emitter_x, int emitter_y, sokoDirection_t emitterDir, + sokoEntity_t* rootEmitter); + +void sokoDoBeam(soko_abs_t* self) +{ + bool receiverImpact; + for (int entInd = 0; entInd < self->currentLevel.entityCount; entInd++) + { + if (self->currentLevel.entities[entInd].type == SKE_LASER_EMIT_UP) + { + self->currentLevel.entities[entInd].properties->targetCount = 0; + receiverImpact = sokoBeamImpactRecursive( + self, self->currentLevel.entities[entInd].x, self->currentLevel.entities[entInd].y, + self->currentLevel.entities[entInd].type, &self->currentLevel.entities[entInd]); + } + } +} + +bool sokoLaserTileCollision(sokoTile_t testTile) +{ + switch (testTile) + { + case SKT_EMPTY: + return false; + case SKT_FLOOR: + return false; + case SKT_WALL: + return true; + case SKT_GOAL: + return false; + case SKT_PORTAL: + return false; + case SKT_FLOOR_WALKED: + return false; + case SKT_NO_WALK: + return false; + default: + return false; + } +} + +bool sokoLaserEntityCollision(sokoEntityType_t testEntity) +{ + switch (testEntity) // Anything that doesn't unconditionally pass should return true + { + case SKE_NONE: + return false; + case SKE_PLAYER: + return false; + case SKE_CRATE: + return true; + case SKE_LASER_90: + return true; + case SKE_STICKY_CRATE: + return true; + case SKE_WARP: + return false; + case SKE_BUTTON: + return false; + case SKE_LASER_EMIT_UP: + return true; + case SKE_LASER_RECEIVE_OMNI: + return true; + case SKE_LASER_RECEIVE: + return true; + case SKE_GHOST: + return true; + default: + return false; + } +} + +sokoDirection_t sokoRedirectDir(sokoDirection_t emitterDir, bool inverted) +{ + switch (emitterDir) + { + case SKD_UP: + return inverted ? SKD_LEFT : SKD_RIGHT; + case SKD_DOWN: + return inverted ? SKD_RIGHT : SKD_LEFT; + case SKD_RIGHT: + return inverted ? SKD_DOWN : SKD_UP; + case SKD_LEFT: + return inverted ? SKD_UP : SKD_DOWN; + default: + return SKD_NONE; + } +} + +int sokoBeamImpactRecursive(soko_abs_t* self, int emitter_x, int emitter_y, sokoDirection_t emitterDir, + sokoEntity_t* rootEmitter) +{ + sokoDirection_t dir = emitterDir; + sokoVec_t projVec = {0, 0}; + sokoVec_t emitVec = {emitter_x, emitter_y}; + switch (dir) + { + case SKD_DOWN: + projVec.y = 1; + break; + case SKD_UP: + projVec.y = -1; + break; + case SKD_LEFT: + projVec.x = -1; + break; + case SKD_RIGHT: + projVec.x = 1; + break; + default: + projVec.y = -1; + break; + // return base entity position + } + + // Iterate over tiles in ray to edge of level + sokoVec_t testPos = sokoAddCoord(emitVec, projVec); + int entityCount = self->currentLevel.entityCount; + // todo: make first pass pack a statically allocated array with only the entities in the path of the laser. + + int16_t possibleSquares = 0; + if (dir == SKD_RIGHT) // move these checks into the switch statement + { + possibleSquares = self->currentLevel.width - emitVec.x; // Up to and including far wall + } + if (dir == SKD_LEFT) + { + possibleSquares = emitVec.x + 1; + } + if (dir == SKD_UP) + { + possibleSquares = emitVec.y + 1; + } + if (dir == SKD_DOWN) + { + possibleSquares = self->currentLevel.height - emitVec.y; + } + + int tileCollFlag, entCollFlag, entCollInd; + tileCollFlag = entCollFlag = entCollInd = 0; + + bool retVal; + // printf("emitVec(%d,%d)",emitVec.x,emitVec.y); + // printf("projVec:(%d,%d) possibleSquares:%d ",projVec.x,projVec.y,possibleSquares); + + for (int n = 0; n < possibleSquares; n++) + { + sokoTile_t posTile = absSokoGetTile(self, testPos.x, testPos.y); + // printf("|n:%d,posTile:(%d,%d):%d|",n,testPos.x,testPos.y,posTile); + if (sokoLaserTileCollision(posTile)) + { + tileCollFlag = 1; + break; + } + for (int m = 0; m < entityCount; m++) // iterate over tiles/entities to check for laser collision. First pass + // finds everything in the path of the + { + sokoEntity_t candidateEntity = self->currentLevel.entities[m]; + // printf("|m:%d;CE:(%d,%d)%d",m,candidateEntity.x,candidateEntity.y,candidateEntity.type); + if (candidateEntity.x == testPos.x && candidateEntity.y == testPos.y) + { + // printf(";POSMATCH;Coll:%d",entityCollision[candidateEntity.type]); + if (sokoLaserEntityCollision(candidateEntity.type)) + { + entCollFlag = 1; + entCollInd = m; + // printf("|"); + break; + } + } + // printf("|"); + } + sokoEntityProperties_t* entProps = rootEmitter->properties; + if (tileCollFlag) + { + entProps->targetX[entProps->targetCount] = testPos.x; // Pack target properties with every impacted + // position. + entProps->targetY[entProps->targetCount] = testPos.y; + entProps->targetCount++; + } + if (entCollFlag) + { + sokoEntityType_t entType = self->currentLevel.entities[entCollInd].type; + + entProps->targetX[entProps->targetCount] = testPos.x; // Pack target properties with every impacted entity. + entProps->targetY[entProps->targetCount] + = testPos.y; // If there's a redirect, it will be added after this one. + entProps->targetCount++; + if (entType == SKE_LASER_90) + { + sokoDirection_t redirectDir + = sokoRedirectDir(emitterDir, self->currentLevel.entities[entCollInd].facing); // SKD_UP or SKD_DOWN + sokoBeamImpactRecursive(self, testPos.x, testPos.y, redirectDir, rootEmitter); + } + + break; + } + testPos = sokoAddCoord(testPos, projVec); + } + retVal = self->currentLevel.entities[entCollInd].properties->targetCount; + // printf("\n"); + // retVal.x = testPos.x; + // retVal.y = testPos.y; + // retVal.entityIndex = entCollInd; + // retVal.entityFlag = entCollFlag; + // printf("impactPoint:(%d,%d)\n",testPos.x,testPos.y); + return retVal; +} + +sokoCollision_t sokoBeamImpact(soko_abs_t* self, sokoEntity_t* emitter) +{ + sokoDirection_t dir = emitter->facing; + sokoVec_t projVec = {0, 0}; + sokoVec_t emitVec = {emitter->x, emitter->y}; + switch (dir) + { + case SKD_DOWN: + projVec.y = 1; + break; + case SKD_UP: + projVec.y = -1; + break; + case SKD_LEFT: + projVec.x = -1; + break; + case SKD_RIGHT: + projVec.x = 1; + break; + default: + // return base entity position + } + + // Iterate over tiles in ray to edge of level + sokoVec_t testPos = sokoAddCoord(emitVec, projVec); + int entityCount = self->currentLevel.entityCount; + // todo: make first pass pack a statically allocated array with only the entities in the path of the laser. + + uint8_t tileCollision[] + = {0, 0, 1, 0, 0, 1, 1}; // There should be a pointer internal to the game state so this can vary with game mode + uint8_t entityCollision[] = {0, 0, 1, 1}; + + int16_t possibleSquares = 0; + if (dir == SKD_RIGHT) // move these checks into the switch statement + { + possibleSquares = self->currentLevel.width - emitVec.x; // Up to and including far wall + } + if (dir == SKD_LEFT) + { + possibleSquares = emitVec.x + 1; + } + if (dir == SKD_UP) + { + possibleSquares = emitVec.y + 1; + } + if (dir == SKD_DOWN) + { + possibleSquares = self->currentLevel.height - emitVec.y; + } + + int tileCollFlag, entCollFlag, entCollInd; + tileCollFlag = entCollFlag = entCollInd = 0; + + sokoCollision_t retVal; + // printf("emitVec(%d,%d)",emitVec.x,emitVec.y); + // printf("projVec:(%d,%d) possibleSquares:%d ",projVec.x,projVec.y,possibleSquares); + + for (int n = 0; n < possibleSquares; n++) + { + sokoTile_t posTile = absSokoGetTile(self, testPos.x, testPos.y); + // printf("|n:%d,posTile:(%d,%d):%d|",n,testPos.x,testPos.y,posTile); + if (tileCollision[posTile]) + { + tileCollFlag = 1; + break; + } + for (int m = 0; m < entityCount; m++) // iterate over tiles/entities to check for laser collision. First pass + // finds everything in the path of the + { + sokoEntity_t candidateEntity = self->currentLevel.entities[m]; + // printf("|m:%d;CE:(%d,%d)%d",m,candidateEntity.x,candidateEntity.y,candidateEntity.type); + if (candidateEntity.x == testPos.x && candidateEntity.y == testPos.y) + { + // printf(";POSMATCH;Coll:%d",entityCollision[candidateEntity.type]); + if (entityCollision[candidateEntity.type]) + { + entCollFlag = 1; + entCollInd = m; + // printf("|"); + break; + } + } + // printf("|"); + } + + if (entCollFlag) + { + break; + } + testPos = sokoAddCoord(testPos, projVec); + } + // printf("\n"); + retVal.x = testPos.x; + retVal.y = testPos.y; + retVal.entityIndex = entCollInd; + retVal.entityFlag = entCollFlag; + // printf("impactPoint:(%d,%d)\n",testPos.x,testPos.y); + return retVal; +} + +sokoVec_t sokoAddCoord(sokoVec_t op1, sokoVec_t op2) +{ + sokoVec_t retVal; + retVal.x = op1.x + op2.x; + retVal.y = op1.y + op2.y; + return retVal; +} + +// Euler Game Modes +void eulerSokoTryPlayerMovement(soko_abs_t* self) +{ + if (self->input.playerInputDeltaX == 0 && self->input.playerInputDeltaY == 0) + { + return; + } + + uint16_t x = self->soko_player->x; + uint16_t y = self->soko_player->y; + bool moved = self->sokoTryMoveEntityInDirectionFunc(self, self->soko_player, self->input.playerInputDeltaX, + self->input.playerInputDeltaY, 0); + + if (moved) + { + // Paint Floor + + // previous + if (self->currentLevel.tiles[x][y] == SKT_FLOOR) + { + self->currentLevel.tiles[x][y] = SKT_FLOOR_WALKED; + } + // current + if (self->currentLevel.tiles[self->soko_player->x][self->soko_player->y] == SKT_FLOOR) + { + self->currentLevel.tiles[self->soko_player->x][self->soko_player->y] = SKT_FLOOR_WALKED; + } + + // Try Sticky Blocks + // Loop through all entities is probably not really slower than sampling? We usually have <5 entities. + for (size_t i = 0; i < self->currentLevel.entityCount; i++) + { + if (self->currentLevel.entities[i].type == SKE_STICKY_CRATE) + { + if (self->currentLevel.entities[i].x == x && self->currentLevel.entities[i].y == y + 1) + { + absSokoTryMoveEntityInDirection(self, &self->currentLevel.entities[i], + self->input.playerInputDeltaX, self->input.playerInputDeltaY, 0); + } + else if (self->currentLevel.entities[i].x == x && self->currentLevel.entities[i].y == y - 1) + { + absSokoTryMoveEntityInDirection(self, &self->currentLevel.entities[i], + self->input.playerInputDeltaX, self->input.playerInputDeltaY, 0); + } + else if (self->currentLevel.entities[i].y == y && self->currentLevel.entities[i].x == x + 1) + { + absSokoTryMoveEntityInDirection(self, &self->currentLevel.entities[i], + self->input.playerInputDeltaX, self->input.playerInputDeltaY, 0); + } + else if (self->currentLevel.entities[i].y == y && self->currentLevel.entities[i].x == x - 1) + { + absSokoTryMoveEntityInDirection(self, &self->currentLevel.entities[i], + self->input.playerInputDeltaX, self->input.playerInputDeltaY, 0); + } + } + } + } +} + +bool eulerNoUnwalkedFloors(soko_abs_t* self) +{ + for (size_t x = 0; x < self->currentLevel.width; x++) + { + for (size_t y = 0; y < self->currentLevel.height; y++) + { + if (self->currentLevel.tiles[x][y] == SKT_FLOOR) + { + return false; + } + } + } + + return true; +} + +void overworldSokoGameLoop(soko_abs_t* self, int64_t elapsedUs) +{ + if (self->state == SKS_GAMEPLAY) + { + // logic + self->sokoTryPlayerMovementFunc(self); + + // victory status. stored separate from gamestate because of future gameplay ideas/remixes. + // todo: rename to isVictory or such. + self->allCratesOnGoal = self->isVictoryConditionFunc(self); + if (self->allCratesOnGoal) + { + self->state = SKS_VICTORY; + printf("Player at %d,%d\n", self->soko_player->x, self->soko_player->y); + victoryDanceTimer = 0; + } + // draw level + self->drawTilesFunc(self, &self->currentLevel); + } + else if (self->state == SKS_VICTORY) + { + self->drawTilesFunc(self, &self->currentLevel); + + // check for input for exit/next level. + uint8_t targetWorldIndex = 0; + for (int i = 0; i < self->portalCount; i++) + { + if (self->soko_player->x == self->portals[i].x && self->soko_player->y == self->portals[i].y) + { + targetWorldIndex = i + 1; + break; + } + } + printf("Player at %d,%d\n", self->soko_player->x, self->soko_player->y); + self->loadNewLevelIndex = targetWorldIndex; + self->loadNewLevelFlag = true; + self->screen = SOKO_LOADNEWLEVEL; + } + + // DEBUG PLACEHOLDER: + // Render the time to a string + char str[16] = {0}; + int16_t tWidth; + if (!self->allCratesOnGoal) + { + snprintf(str, sizeof(str) - 1, "sokoban"); + // Measure the width of the time string + tWidth = textWidth(&self->ibm, str); + // Draw the time string to the display, centered at (TFT_WIDTH / 2) + drawText(&self->ibm, c555, str, ((TFT_WIDTH - tWidth) / 2), 0); + } + else + { + snprintf(str, sizeof(str) - 1, "sokasuccess"); + // Measure the width of the time string + tWidth = textWidth(&self->ibm, str); + // Draw the time string to the display, centered at (TFT_WIDTH / 2) + drawText(&self->ibm, c555, str, ((TFT_WIDTH - tWidth) / 2), 0); + } +} + +bool overworldPortalEntered(soko_abs_t* self) +{ + for (uint8_t i = 0; i < self->portalCount; i++) + { + if (self->soko_player->x == self->portals[i].x && self->soko_player->y == self->portals[i].y) + { + return true; + } + } + return false; +} + +void restartCurrentLevel(soko_abs_t* self) +{ + // assumed this is set already? + // self->loadNewLevelIndex = self->loadNewLevelIndex; + + // todo: what can we do about screen flash when restarting? + self->loadNewLevelFlag = true; + self->screen = SOKO_LOADNEWLEVEL; +} + +void exitToOverworld(soko_abs_t* self) +{ + self->loadNewLevelIndex = 0; + self->loadNewLevelFlag = true; + // self->state = SKS_GAMEPLAY; + self->screen = SOKO_LOADNEWLEVEL; +} diff --git a/main/modes/soko/soko_gamerules.h b/main/modes/soko/soko_gamerules.h new file mode 100644 index 000000000..6af8af367 --- /dev/null +++ b/main/modes/soko/soko_gamerules.h @@ -0,0 +1,43 @@ +#ifndef SOKO_GAMERULES_H +#define SOKO_GAMERULES_H + +/// @brief call [entity][tile] to get a bool that is true if that entity can NOT walk (or get pushed onto) that tile. +// bool sokoEntityTileCollision[4][8]; + +sokoTile_t sokoGetTile(int, int); +void sokoConfigGamemode(soko_abs_t* gamestate, soko_var_t variant); + +// utility/shared functions. +void sharedGameLoop(soko_abs_t* self); +sokoDirection_t sokoDirectionFromDelta(int, int); + +// entity pushing. +void sokoTryPlayerMovement(void); +bool sokoTryMoveEntityInDirection(sokoEntity_t*, int, int, uint16_t); + +// classic and default +void absSokoGameLoop(soko_abs_t* self, int64_t elapsedUs); +void absSokoTryPlayerMovement(soko_abs_t* self); +bool absSokoTryMoveEntityInDirection(soko_abs_t* self, sokoEntity_t* entity, int dx, int dy, uint16_t push); +void absSokoDrawTiles(soko_abs_t* self, sokoLevel_t* level); +bool absSokoAllCratesOnGoal(soko_abs_t* self); +sokoTile_t absSokoGetTile(soko_abs_t* self, int x, int y); +bool allCratesOnGoal(void); + +void laserBounceSokoGameLoop(soko_abs_t* self, int64_t elapsedUs); +sokoVec_t sokoGridToPix(soko_abs_t* self, sokoVec_t grid); +void drawLaserFromEntity(soko_abs_t* self, sokoEntity_t* emitter); +sokoCollision_t sokoBeamImpact(soko_abs_t* self, sokoEntity_t* emitter); +sokoVec_t sokoAddCoord(sokoVec_t op1, sokoVec_t op2); + +// euler +void eulerSokoTryPlayerMovement(soko_abs_t* self); +bool eulerNoUnwalkedFloors(soko_abs_t* self); + +// overworld +void overworldSokoGameLoop(soko_abs_t* self, int64_t elapsedUs); +bool overworldPortalEntered(soko_abs_t* self); +void restartCurrentLevel(soko_abs_t* self); +void exitToOverworld(soko_abs_t* self); + +#endif // SOKO_GAMERULES_H \ No newline at end of file diff --git a/main/modes/soko/soko_input.c b/main/modes/soko/soko_input.c new file mode 100644 index 000000000..4392d9c2e --- /dev/null +++ b/main/modes/soko/soko_input.c @@ -0,0 +1,151 @@ +#include "soko_input.h" + +/** + * @brief Initialize Input. Does this for every puzzle start, to reset button state. + * Also where config like dastime is set. + * + * @param input + */ +void sokoInitInput(sokoGameplayInput_t* input) +{ + input->dasTime = 100000; + input->firstDASTime = 500000; + input->DASActive = false; + input->prevHoldingDir = SKD_NONE; + input->prevBtnState = 0; + input->playerInputDeltaX = 0; + input->playerInputDeltaY = 0; + input->restartLevel = false; + input->exitToOverworld = false; +} +/** + * @brief Input preprocessing turns btnstate into game-logic usable data. + * Input variables only set on press, as appropriate. + * Handles DAS, settings, etc. + * Called once a frame before game loop. + * + * @param input + */ +void sokoPreProcessInput(sokoGameplayInput_t* input, int64_t elapsedUs) +{ + uint16_t btn = input->btnState; + // reset output data. + input->playerInputDeltaY = 0; + input->playerInputDeltaX = 0; + + // Non directional buttons + if ((btn & PB_B) && !(input->prevBtnState & PB_B)) + { + input->restartLevel = true; + } // else set to false, but this won't matter when level reloads. + + if ((btn & PB_START) && !(input->prevBtnState & PB_START)) + { + input->exitToOverworld = true; + } // else set to false, but this won't matter when we quit + + // update holding direction + if ((btn & PB_UP) && !(btn & 0b1110)) + { + input->holdingDir = SKD_UP; + } + else if ((btn & PB_DOWN) && !(btn & 0b1101)) + { + input->holdingDir = SKD_DOWN; + } + else if ((btn & PB_LEFT) && !(btn & 0b1011)) + { + input->holdingDir = SKD_LEFT; + } + else if ((btn & PB_RIGHT) && !(btn & 0b0111)) + { + input->holdingDir = SKD_RIGHT; + } + else + { + input->holdingDir = SKD_NONE; + input->DASActive = false; + input->timeHeldDirection = 0; // reset when buttons change or multiple buttons. + } + + // going from one button to another without letting go could cheese DAS. + if (input->holdingDir != input->prevHoldingDir) + { + input->DASActive = false; + input->timeHeldDirection = 0; + } + + // increment DAS time. + if (input->holdingDir != SKD_NONE) + { + input->timeHeldDirection += elapsedUs; + } + + // two cases when DAS gets triggered: initial and every one after the initial. + bool triggerDAS = false; + if (input->DASActive == false && input->timeHeldDirection > input->firstDASTime) + { + triggerDAS = true; + input->DASActive = true; + } + + if (input->DASActive == true && input->timeHeldDirection > input->dasTime) + { + triggerDAS = true; + } + + if (triggerDAS) + { + // reset timer + input->timeHeldDirection = 0; + + // trigger movement + // todo: in sokogame i had to write delta to direction. This is basically directionenum to delta, which could be + // extracted too. + switch (input->holdingDir) + { + case SKD_RIGHT: + input->playerInputDeltaX = 1; + break; + case SKD_LEFT: + input->playerInputDeltaX = -1; + break; + case SKD_UP: + input->playerInputDeltaY = -1; + break; + case SKD_DOWN: + input->playerInputDeltaY = 1; + break; + case SKD_NONE: + default: + break; + } + } + else + { // if !trigger DAS + + // holdingDir is ONLY holding one button. So we use normal buttonstate for taps so we can tap button two before + // releasing button one. + if (input->btnState & PB_UP && !(input->prevBtnState & PB_UP)) + { + input->playerInputDeltaY = -1; + } + else if (input->btnState & PB_DOWN && !(input->prevBtnState & PB_DOWN)) + { + input->playerInputDeltaY = 1; + } + else if (input->btnState & PB_LEFT && !(input->prevBtnState & PB_LEFT)) + { + input->playerInputDeltaX = -1; + } + else if (input->btnState & PB_RIGHT && !(input->prevBtnState & PB_RIGHT)) + { + input->playerInputDeltaX = 1; + } + + } // end !triggerDAS + + // do this last + input->prevBtnState = btn; + input->prevHoldingDir = input->holdingDir; +} diff --git a/main/modes/soko/soko_input.h b/main/modes/soko/soko_input.h new file mode 100644 index 000000000..f3abfb355 --- /dev/null +++ b/main/modes/soko/soko_input.h @@ -0,0 +1,39 @@ +#include "swadge2024.h" + +// there is a way to set clever ints here such that we can super quickly convert to dx and dy with bit ops. I'll think +// it through eventually. +typedef enum +{ + SKD_UP, + SKD_DOWN, + SKD_RIGHT, + SKD_LEFT, + SKD_NONE +} sokoDirection_t; + +typedef struct +{ + // input input data. + uint16_t btnState; ///< The button state. Provided to input For PreProcess. + + // input meta data. Used by PreProcess. + uint16_t prevBtnState; ///< The button state from the previous frame. + sokoDirection_t holdingDir; ///< What direction we are holding down. + sokoDirection_t prevHoldingDir; ///< What direction we are holding down. + uint64_t timeHeldDirection; ///< The amount of time we have been holding a single button down. Used for DAS. + bool DASActive; ///< If DAS has begun. User may be holding before first DAS, this is false. After first, it becomes + ///< true. + uint64_t dasTime; ///< How many microseconds before DAS starts + uint64_t firstDASTime; ///< how many microseconds after DAS has started before the next DAS + + // input output data. ie: usable Gameplay data. + // todo: use Direction in input + int playerInputDeltaX; + int playerInputDeltaY; + bool restartLevel; + bool exitToOverworld; + +} sokoGameplayInput_t; + +void sokoInitInput(sokoGameplayInput_t*); +void sokoPreProcessInput(sokoGameplayInput_t*, int64_t); diff --git a/main/modes/tunernome/tunernome.c b/main/modes/tunernome/tunernome.c index d97ca2f11..0e6d958e3 100644 --- a/main/modes/tunernome/tunernome.c +++ b/main/modes/tunernome/tunernome.c @@ -284,7 +284,7 @@ static songTrack_t metronome_primary_tracks[] = {{ .loopStartNote = 0, .notes = metronome_primary_notes, }}; -const song_t metronome_primary +song_t metronome_primary = {.numTracks = ARRAY_SIZE(metronome_primary_tracks), .shouldLoop = false, .tracks = metronome_primary_tracks}; static musicalNote_t metronome_secondary_notes[] = {{ @@ -296,7 +296,7 @@ static songTrack_t metronome_secondary_tracks[] = {{ .loopStartNote = 0, .notes = metronome_secondary_notes, }}; -const song_t metronome_secondary +song_t metronome_secondary = {.numTracks = ARRAY_SIZE(metronome_secondary_tracks), .shouldLoop = false, .tracks = metronome_secondary_tracks}; /*============================================================================ @@ -599,7 +599,7 @@ void instrumentTunerMagic(const uint16_t freqBinIdxs[], uint16_t numStrings, led { // Note too sharp, make it red red = 255; - grn = blu = 255 - (tonalDiff)*15; + grn = blu = 255 - (tonalDiff) * 15; } else { diff --git a/main/modes/tunernome/tunernome.h b/main/modes/tunernome/tunernome.h index 0e5447b11..61c7ec81d 100644 --- a/main/modes/tunernome/tunernome.h +++ b/main/modes/tunernome/tunernome.h @@ -12,7 +12,7 @@ extern swadgeMode_t tunernomeMode; -extern const song_t metronome_primary; -extern const song_t metronome_secondary; +extern song_t metronome_primary; +extern song_t metronome_secondary; #endif /* _MODE_TUNERNOME_H_ */ \ No newline at end of file diff --git a/main/swadge2024.c b/main/swadge2024.c index 4e36ad74d..ae49d47d1 100644 --- a/main/swadge2024.c +++ b/main/swadge2024.c @@ -118,7 +118,7 @@ * developers to write modes and games for the Swadge without going too deep into Espressif's API. However, if you're * doing system development or writing a mode that requires a specific hardware peripheral, this Espressif documentation * is useful: - * - ESP-IDF API + * - ESP-IDF API * Reference * - ESP32-­S2 Series * Datasheet @@ -142,6 +142,7 @@ #include "advanced_usb_control.h" #include "swadge2024.h" #include "mainMenu.h" +#include "lumberjack.h" #include "quickSettings.h" #include "shapes.h" @@ -156,6 +157,7 @@ #define EXIT_TIME_US 1000000 #define PAUSE_TIME_US 500000 +#define DEFAULT_FRAME_RATE_US 40000 //============================================================================== // Variables @@ -167,18 +169,15 @@ static swadgeMode_t* cSwadgeMode = &mainMenuMode; /// @brief A pending Swadge mode to use after a deep sleep static RTC_DATA_ATTR swadgeMode_t* pendingSwadgeMode = NULL; -/// @brief Whether or not the quck settings overlay mode is shown +/// @brief Whether or not the quick settings overlay mode is shown static bool showQuickSettings = false; /// 25 FPS by default -static uint32_t frameRateUs = 40000; +static uint32_t frameRateUs = DEFAULT_FRAME_RATE_US; /// @brief Timer to return to the main menu static int64_t timeExitPressed = 0; -/// @brief Timer to open quick settings menu -static int64_t timePausePressed = 0; - //============================================================================== // Function declarations //============================================================================== @@ -281,7 +280,8 @@ void app_main(void) // Init esp-now if requested by the mode if ((ESP_NOW == cSwadgeMode->wifiMode) || (ESP_NOW_IMMEDIATE == cSwadgeMode->wifiMode)) { - initEspNow(&swadgeModeEspNowRecvCb, &swadgeModeEspNowSendCb, GPIO_NUM_NC, GPIO_NUM_NC, UART_NUM_MAX, ESP_NOW); + initEspNow(&swadgeModeEspNowRecvCb, &swadgeModeEspNowSendCb, GPIO_NUM_NC, GPIO_NUM_NC, UART_NUM_MAX, + cSwadgeMode->wifiMode); } // Init accelerometer @@ -349,6 +349,11 @@ void app_main(void) } } + if (NO_WIFI != cSwadgeMode->wifiMode) + { + checkEspNowRxQueue(); + } + // Only draw to the TFT every frameRateUs static uint64_t tAccumDraw = 0; tAccumDraw += tElapsedUs; @@ -384,7 +389,7 @@ void app_main(void) if (0 != timeExitPressed && !showQuickSettings) { // Figure out for how long - int64_t tHeldUs = tNowUs - timeExitPressed; + int64_t tHeldUs = esp_timer_get_time() - timeExitPressed; // If it has been held for more than the exit time if (tHeldUs > EXIT_TIME_US) { @@ -400,37 +405,6 @@ void app_main(void) fillDisplayArea(0, TFT_HEIGHT - 10, numPx, TFT_HEIGHT, c333); } } - else if (0 != timePausePressed) - { - int64_t tHeldUs = tNowUs - timePausePressed; - - if (tHeldUs > PAUSE_TIME_US) - { - if (showQuickSettings) - { - // Quick settings is active, just quit that - quickSettingsMode.fnExitMode(); - showQuickSettings = false; - } - else - { - // Quick settings not active, set it up - quickSettingsMode.fnEnterMode(); - showQuickSettings = true; - } - - // Reset the count - timePausePressed = 0; - } - else - { - int16_t r = QUICK_SETTINGS_PANEL_R; - int16_t numPx = (tHeldUs * (QUICK_SETTINGS_PANEL_W - r * 2)) / PAUSE_TIME_US; - drawCircleFilled(QUICK_SETTINGS_PANEL_X + r, 0, r, c333); - fillDisplayArea(QUICK_SETTINGS_PANEL_X + r, 0, QUICK_SETTINGS_PANEL_X + r + numPx, r + 1, c333); - drawCircleFilled(QUICK_SETTINGS_PANEL_X + numPx + r, 0, r, c333); - } - } // Draw to the TFT drawDisplayTft(showQuickSettings ? NULL : cSwadgeMode->fnBackgroundDrawCallback); @@ -560,6 +534,9 @@ static void setSwadgeMode(void* swadgeMode) */ void switchToSwadgeMode(swadgeMode_t* mode) { + //Set the framerate back to default + setFrameRateUs(DEFAULT_FRAME_RATE_US); + pendingSwadgeMode = mode; } @@ -612,46 +589,53 @@ void softSwitchToPendingSwadge(void) */ bool checkButtonQueueWrapper(buttonEvt_t* evt) { + // Check the button queue bool retval = checkButtonQueue(evt); - // Intercept button presses for PB_SELECT - if (retval) + // Check for intercept + if (retval && // If there was a button press + (cSwadgeMode != &mainMenuMode) && // And this isn't the main menu + (evt->button == PB_SELECT)) // And the button was PB_SELECT { - // Don't intercept the button on the main menu - if (cSwadgeMode != &mainMenuMode) + if (evt->down) + { + // Button was pressed, start the timer + timeExitPressed = esp_timer_get_time(); + } + else { - if (evt->button == PB_SELECT && !showQuickSettings) + // Button was released, stop the timer + timeExitPressed = 0; + + // If the mode hasn't exited yet, toggle quick settings + if (false == showQuickSettings) { - if (evt->down) - { - // Button was pressed, start the timer - timeExitPressed = esp_timer_get_time(); - } - else - { - // Button was released, stop the timer - timeExitPressed = 0; - } + // Show the quick settings + quickSettingsMode.fnEnterMode(); + showQuickSettings = true; } - else if (evt->button == PB_START && !timeExitPressed) + else { - // Handle the start button for the quick-settings menu, - // but only if we're not already handling select and the - // quick-settings menu is not already enabled - if (evt->down) - { - // Button was pressed, start the timer - timePausePressed = esp_timer_get_time(); - } - else - { - // Button was relesaed, stop the timer - timePausePressed = 0; - } + // Hide the quick settings + showQuickSettings = false; + quickSettingsMode.fnExitMode(); } } + + // Don't pass this button to the mode + retval = false; } // Return if there was an event or not return retval; } + +/** + * @brief Set the framerate, in microseconds + * + * @param newFrameRateUs The time between frame draws, in microseconds + */ +void setFrameRateUs(uint32_t newFrameRateUs) +{ + frameRateUs = newFrameRateUs; +} \ No newline at end of file diff --git a/main/swadge2024.h b/main/swadge2024.h index 38d08082b..7a6640cb4 100644 --- a/main/swadge2024.h +++ b/main/swadge2024.h @@ -30,6 +30,21 @@ * * \section swadgeMode_example Example * + * Adding a mode to the CMakeFile requires adding two separate lines in the idf_component_register section. + * + * \code{.c} + * "modes/pong/pong.c" + * \endcode + * + * under the SRCS section and + * + * \code{.c} + * "modes/pong" + * \endcode + * + * under the INCLUDES section. + * + * * Function prototypes must be declared before using them to initialize function pointers: * \code{.c} * // It's good practice to declare immutable strings as const so they get placed in ROM, not RAM @@ -189,6 +204,7 @@ #include "vector2d.h" #include "geometry.h" #include "settingsManager.h" +#include "touchUtils.h" /** * @struct swadgeMode_t @@ -306,4 +322,6 @@ void softSwitchToPendingSwadge(void); void deinitSystem(void); +void setFrameRateUs(uint32_t newFrameRateUs); + #endif diff --git a/main/utils/macros.h b/main/utils/macros.h index 504c9dbc7..243cf70f7 100644 --- a/main/utils/macros.h +++ b/main/utils/macros.h @@ -56,8 +56,14 @@ /** * @brief Returns (a + b) % d, but with negative values converted to equivalent positive values. * The resulting value will always be in the range [0, d), assuming d > 0. + * + * The first modulo, (b % d) will return e.g. -90 for (-270 % 360) + * + * @param a One number to sum + * @param b Another number to sum + * @param d The number to mod the sum by + * @return (a + b) % d */ -// The first modulo, (b % d) will return e.g. -90 for (-270 % 360) #define POS_MODULO_ADD(a, b, d) ((a + (b % d) + d) % d) #endif \ No newline at end of file diff --git a/main/utils/p2pConnection.h b/main/utils/p2pConnection.h index 616dd57a0..41cdfa6a9 100644 --- a/main/utils/p2pConnection.h +++ b/main/utils/p2pConnection.h @@ -12,7 +12,7 @@ * The play order is determined by who acknowledges the start message first, which is suitably random. * A connection sequence looks like this: * - * @startuml + * @startuml{conn_seq.png} "Connection Sequence" * == Connection == * * group Part 1 @@ -35,17 +35,17 @@ * After connection, Swadges are free to send messages to each other. * An example of unreliable communication with retries and duplication is as follows. * - * @startuml + * @startuml{unre_comm.png} "Unreliable Communication" * == Unreliable Communication Example == * * group Retries & Sequence Numbers - * "Swadge_AB:AB:AB:AB:AB:AB" ->x "Swadge_12:12:12:12:12:12" : "['p', {mode ID}, 0x03 {P2P_MSG_DATA}, 0x04 {seqNum}, (0x12, 0x12, 0x12, 0x12, 0x12, 0x12), 'd', 'a', 't', 'a'] + * "Swadge_AB:AB:AB:AB:AB:AB" ->x "Swadge_12:12:12:12:12:12" : "['p', {mode ID}, 0x04 {P2P_MSG_DATA}, 0x04 {seqNum}, (0x12, 0x12, 0x12, 0x12, 0x12, 0x12), 'd', 'a', 't', 'a'] * note right: msg not received - * "Swadge_AB:AB:AB:AB:AB:AB" -> "Swadge_12:12:12:12:12:12" : "['p', {mode ID}, 0x03 {P2P_MSG_DATA}, 0x04 {seqNum}, (0x12, 0x12, 0x12, 0x12, 0x12, 0x12), 'd', 'a', 't', 'a'] + * "Swadge_AB:AB:AB:AB:AB:AB" -> "Swadge_12:12:12:12:12:12" : "['p', {mode ID}, 0x04 {P2P_MSG_DATA}, 0x04 {seqNum}, (0x12, 0x12, 0x12, 0x12, 0x12, 0x12), 'd', 'a', 't', 'a'] * note left: first retry, up to five retries * "Swadge_12:12:12:12:12:12" ->x "Swadge_AB:AB:AB:AB:AB:AB" : "['p', {mode ID}, 0x02 {P2P_MSG_ACK}, 0x04 {seqNum}, (0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB)] * note left: ack not received - * "Swadge_AB:AB:AB:AB:AB:AB" -> "Swadge_12:12:12:12:12:12" : "['p', {mode ID}, 0x03 {P2P_MSG_DATA}, 0x04 {seqNum}, (0x12, 0x12, 0x12, 0x12, 0x12, 0x12), 'd', 'a', 't', 'a'] + * "Swadge_AB:AB:AB:AB:AB:AB" -> "Swadge_12:12:12:12:12:12" : "['p', {mode ID}, 0x04 {P2P_MSG_DATA}, 0x04 {seqNum}, (0x12, 0x12, 0x12, 0x12, 0x12, 0x12), 'd', 'a', 't', 'a'] * note left: second retry * note right: duplicate seq num, ignore message * "Swadge_12:12:12:12:12:12" -> "Swadge_AB:AB:AB:AB:AB:AB" : "['p', {mode ID}, 0x02 {P2P_MSG_ACK}, 0x05 {seqNum}, (0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB)] diff --git a/main/utils/trigonometry.c b/main/utils/trigonometry.c index 44d7fbfcc..63d846a1e 100644 --- a/main/utils/trigonometry.c +++ b/main/utils/trigonometry.c @@ -161,6 +161,94 @@ int32_t getTan1024(int16_t degree) } } +/** + * @brief CORDIC approximation of arctan + * + * @param x The x component + * @param y The y component + * @return The approximation of atan(y/x), in degrees + */ +int32_t cordicAtan2(int32_t x, int32_t y) +{ + // Check for 90 degree angles first + if (x == 0) + { + if (y > 0) + { + return 0; + } + else + { + return 180; + } + } + else if (y == 0) + { + if (x > 0) + { + return 90; + } + else + { + return 270; + } + } + + // Constants for speed, it's sin(128), sin(64), sin(32), etc. + const int16_t cSin1024[] = {807, 920, 543, 282, 143, 71, 36, 18}; + const int16_t cCos1024[] = {-630, 449, 868, 984, 1014, 1022, 1023, 1024}; + + // X and Y coordinates after rotation + int32_t xNew; + int32_t yNew; + // The cumulative rotation + int16_t sumAngle = 0; + + // Scale these up for precision + x *= 1024; + y *= 1024; + + // Pretty much a binary search trying to get x to 0 + int16_t loop = 0; + for (int16_t angle = 128; angle > 0; angle /= 2) + { + if (x > 0) + { + // rotate counterclockwise + xNew = (x * cCos1024[loop]) - (y * cSin1024[loop]); + yNew = (y * cCos1024[loop]) + (x * cSin1024[loop]); + sumAngle += angle; + } + else + { + // rotate clockwise + xNew = (x * cCos1024[loop]) + (y * cSin1024[loop]); + yNew = (y * cCos1024[loop]) - (x * cSin1024[loop]); + sumAngle -= angle; + } + + // Scale down the factor from sin1024 and cos1024 + x = xNew / 1024; + y = yNew / 1024; + + // Increment the loop for the angle LUTs + loop++; + } + + // Make sure the returned angle is within [0,359] + while (sumAngle < 0) + { + sumAngle += 360; + } + while (sumAngle >= 360) + { + sumAngle -= 360; + } + + // Return it + return sumAngle; +} + /** * @brief Static helper function to make sure the ::ARCTAN_APPROX macro * is only called with parameters inside its domain. @@ -231,4 +319,4 @@ int16_t getAtan2(int32_t y, int32_t x) { return innerAtan2(y, x); } -} \ No newline at end of file +} diff --git a/main/utils/trigonometry.h b/main/utils/trigonometry.h index 5350abc02..a43db550e 100644 --- a/main/utils/trigonometry.h +++ b/main/utils/trigonometry.h @@ -34,6 +34,7 @@ extern const uint16_t tan1024[91]; int16_t getSin1024(int16_t degree); int16_t getCos1024(int16_t degree); int32_t getTan1024(int16_t degree); +int32_t cordicAtan2(int32_t x, int32_t y); int16_t getAtan2(int32_t y, int32_t x); #endif \ No newline at end of file diff --git a/main/utils/wheel_menu.c b/main/utils/wheel_menu.c new file mode 100644 index 000000000..ad3c870c3 --- /dev/null +++ b/main/utils/wheel_menu.c @@ -0,0 +1,575 @@ +//============================================================================== +// Includes +//============================================================================== + +#include "wheel_menu.h" + +#include "hdw-tft.h" +#include "shapes.h" +#include "fill.h" +#include "trigonometry.h" +#include "menu.h" +#include "wsg.h" +#include "esp_log.h" + +#include +#include +#include + +//============================================================================== +// Structs +//============================================================================== + +typedef struct +{ + const char* label; + const wsg_t* icon; + uint8_t position; + wheelScrollDir_t scroll; + paletteColor_t selectedBg; + paletteColor_t unselectedBg; +} wheelItemInfo_t; + +//============================================================================== +// Function Prototypes +//============================================================================== + +static wheelItemInfo_t* findInfo(wheelMenuRenderer_t* renderer, const char* label); + +//============================================================================== +// Functions +//============================================================================== + +/** + * @brief Initializes and returns a new wheel menu with the default settings + * + * @param font The font to draw menu item labels with + * @param anchorAngle The angle where the center of the first item will be anchored + * @param textBox TODO doc + * @return TODO doc + */ +wheelMenuRenderer_t* initWheelMenu(const font_t* font, uint16_t anchorAngle, const rectangle_t* textBox) +{ + wheelMenuRenderer_t* renderer = calloc(1, sizeof(wheelMenuRenderer_t)); + + renderer->font = font; + renderer->anchorAngle = anchorAngle; + renderer->textBox = textBox; + + renderer->textColor = c000; + renderer->borderColor = c000; + renderer->unselBgColor = c333; + renderer->selBgColor = c555; + + renderer->centerR = TFT_HEIGHT / 12; + renderer->unselR = TFT_HEIGHT / 4; + renderer->selR = TFT_HEIGHT / 3; + + renderer->x = TFT_WIDTH / 2; + renderer->y = TFT_HEIGHT / 2; + + renderer->active = false; + + return renderer; +} + +/** + * @brief TODO doc + * + * @param renderer + */ +void deinitWheelMenu(wheelMenuRenderer_t* renderer) +{ + node_t* info = NULL; + while ((info = pop(&renderer->itemInfos))) + { + free(info); + } + + free(renderer); +} + +/** + * @brief Sets the icon and ring position for the item with the given label + * + * @param renderer TODO doc + * @param label The label of the item to set the icon and position for + * @param icon The icon to use when drawing the menu item + * @param position The order of the item in the ring, centered around the anchor angle + * @param scrollDir Which axis this item should be scrollable in + */ +void wheelMenuSetItemInfo(wheelMenuRenderer_t* renderer, const char* label, const wsg_t* icon, uint8_t position, + wheelScrollDir_t scrollDir) +{ + wheelItemInfo_t* info = findInfo(renderer, label); + + if (NULL == info) + { + info = malloc(sizeof(wheelItemInfo_t)); + info->label = label; + + // Defaults for colors + info->unselectedBg = renderer->unselBgColor; + info->selectedBg = renderer->selBgColor; + + // We only add the item to the list if it's not already there + push(&renderer->itemInfos, info); + } + + info->icon = icon; + info->position = position; + info->scroll = scrollDir; + + if (label == mnuBackStr) + { + renderer->customBack = true; + } +} + +/** + * @brief TODO doc + * + * @param renderer + * @param label + * @param selectedBg + * @param unselectedBg + */ +void wheelMenuSetItemColor(wheelMenuRenderer_t* renderer, const char* label, paletteColor_t selectedBg, + paletteColor_t unselectedBg) +{ + wheelItemInfo_t* info = findInfo(renderer, label); + + if (NULL == info) + { + // Initialize everything not set to NULL / 0 + info = calloc(1, sizeof(wheelItemInfo_t)); + info->label = label; + + push(&renderer->itemInfos, info); + } + + info->unselectedBg = unselectedBg; + info->selectedBg = selectedBg; +} + +/** + * @brief TODO doc + * + * @param menu + * @param renderer + * @param elapsedUs + */ +void drawWheelMenu(menu_t* menu, wheelMenuRenderer_t* renderer, int64_t elapsedUs) +{ + node_t* node = menu->items->first; + + // Draw background circle for the unselected area + drawCircleFilled(renderer->x, renderer->y, renderer->unselR, renderer->unselBgColor); + + uint16_t centerR = renderer->centerR; + uint16_t fillAngle = 0; + paletteColor_t selBgColor = renderer->selBgColor; + + // If we haven't customized the back option to be around the ring, then we use the center + // So, remove one from the ring items to compensate + uint8_t ringItems = menu->items->length - ((menu->parentMenu && !renderer->customBack) ? 1 : 0); + uint16_t anchorAngle = (360 + renderer->anchorAngle - (360 / ringItems / 2)) % 360; + + // We need to draw the WSGs after all the fills, so just store the locations to avoid calculating twice + struct + { + uint16_t x; + uint16_t y; + const wsg_t* wsg; + paletteColor_t bgColor; + } iconsToDraw[ringItems]; + uint8_t wsgs = 0; + + while (node != NULL) + { + menuItem_t* item = node->val; + // Find menu item by either its label or its first option label + wheelItemInfo_t* info = findInfo(renderer, item->label ? item->label : *item->options); + + if (info != NULL) + { + // Calculate where this sector starts + uint16_t startAngle = (anchorAngle + info->position * 360 / ringItems) % 360; + uint16_t endAngle = (anchorAngle + (info->position + 1) * 360 / ringItems) % 360; + + // We'll use the center angle for drawing icons, filling in, etc. + uint16_t centerAngle = ((startAngle < endAngle) ? (startAngle + (endAngle - startAngle) / 2) + : (startAngle + ((endAngle + 360) - startAngle) / 2)) + % 360; + + uint16_t r = (renderer->touched && menu->currentItem == node) ? renderer->selR : renderer->unselR; + + drawLine(renderer->x + getCos1024(startAngle) * centerR / 1024, + renderer->y - getSin1024(startAngle) * centerR / 1024, + renderer->x + getCos1024(startAngle) * r / 1024, renderer->y - getSin1024(startAngle) * r / 1024, + renderer->borderColor, 0); + + drawLine(renderer->x + getCos1024(endAngle) * centerR / 1024, + renderer->y - getSin1024(endAngle) * centerR / 1024, renderer->x + getCos1024(endAngle) * r / 1024, + renderer->y - getSin1024(endAngle) * r / 1024, renderer->borderColor, 0); + + // If there's an icon, or the item's eventual color doesn't match the normal BG color + if (info->icon + || ((renderer->touched && menu->currentItem == node) ? (info->selectedBg != selBgColor) + : (info->unselectedBg != renderer->unselBgColor))) + { + iconsToDraw[wsgs].x = renderer->x + getCos1024(centerAngle) * (centerR + (r - centerR) / 2) / 1024 + - (info->icon ? info->icon->w / 2 : 0); + iconsToDraw[wsgs].y = renderer->y - getSin1024(centerAngle) * (centerR + (r - centerR) / 2) / 1024 + - (info->icon ? info->icon->h / 2 : 0); + iconsToDraw[wsgs].wsg = info->icon; + iconsToDraw[wsgs].bgColor + = (renderer->touched && menu->currentItem == node) ? info->selectedBg : info->unselectedBg; + ++wsgs; + } + + for (uint16_t ang = startAngle; ang != endAngle; ang = (ang + 1) % 360) + { + drawLine(renderer->x + getCos1024(ang) * (r + 1) / 1024, renderer->y - getSin1024(ang) * (r + 1) / 1024, + renderer->x + getCos1024((ang + 1) % 360) * (r + 1) / 1024, + renderer->y - getSin1024((ang + 1) % 360) * (r + 1) / 1024, renderer->borderColor, 0); + } + + if (renderer->touched && menu->currentItem == node) + { + if (info->selectedBg != selBgColor) + { + selBgColor = info->selectedBg; + } + + fillAngle = centerAngle; + } + } + + node = node->next; + } + + if (!renderer->touched || !menu->currentItem || (!renderer->customBack && menuItemIsBack(menu->currentItem->val))) + { + // Here, we handle the case that the + + if (renderer->touched) + { + // This special case is just to fill faster than flood fill, it should work fine + drawCircleFilled(renderer->x, renderer->y, centerR, selBgColor); + } + + // draw the center circle border after + drawCircle(renderer->x, renderer->y, centerR, renderer->borderColor); + } + else + { + // Draw the center circle first to establish bounds for the fill + drawCircle(renderer->x, renderer->y, centerR, renderer->borderColor); + + // Color the background under the selected item, first the inner part + floodFill(renderer->x + getCos1024(fillAngle) * (centerR + (renderer->unselR - centerR) / 2) / 1024, + renderer->y - getSin1024(fillAngle) * (centerR + (renderer->unselR - centerR) / 2) / 1024, selBgColor, + renderer->x - renderer->selR, renderer->y - renderer->selR, renderer->x + renderer->selR, + renderer->y + renderer->selR); + + // And then color the outer part of the selected item + floodFill( + renderer->x + getCos1024(fillAngle) * (renderer->unselR + (renderer->selR - renderer->unselR) / 2) / 1024, + renderer->y - getSin1024(fillAngle) * (renderer->unselR + (renderer->selR - renderer->unselR) / 2) / 1024, + selBgColor, renderer->x - renderer->selR, renderer->y - renderer->selR, renderer->x + renderer->selR, + renderer->y + renderer->selR); + } + + for (uint8_t i = 0; i < wsgs; i++) + { + if (iconsToDraw[i].bgColor != renderer->unselBgColor) + { + // Fill in the background + floodFill(iconsToDraw[i].x, iconsToDraw[i].y, iconsToDraw[i].bgColor, renderer->x - renderer->selR, + renderer->y - renderer->selR, renderer->x + renderer->selR, renderer->y + renderer->selR); + } + + if (iconsToDraw[i].wsg) + { + drawWsgSimple(iconsToDraw[i].wsg, iconsToDraw[i].x, iconsToDraw[i].y); + } + } + + if (renderer->textBox && menu->currentItem && renderer->touched) + { + char buffer[128] = {0}; + const char* label = getMenuItemLabelText(buffer, sizeof(buffer) - 1, menu->currentItem->val); + + uint16_t textW = textWidth(renderer->font, label); + + while (textW > renderer->textBox->width) + { + // Copy the real label first if we're using a static string + if (label != buffer) + { + snprintf(buffer, sizeof(buffer) - 1, "%s", label); + label = buffer; + } + + char* ptr = buffer + strlen(buffer); + // Shorten the text by one, and add trailing + *ptr-- = '\0'; + + for (uint8_t i = 0; i < 3 && ptr > buffer; i++) + { + *ptr-- = '.'; + } + + textW = textWidth(renderer->font, label); + } + + drawText(renderer->font, renderer->textColor, label, + renderer->textBox->x + (renderer->textBox->width - textW) / 2, + renderer->textBox->y + (renderer->textBox->height - renderer->font->height - 1) / 2); + } +} + +/** + * @brief TODO doc + * + * @param menu + * @param renderer + * @param angle + * @param radius + * @return menu_t* + */ +menu_t* wheelMenuTouch(menu_t* menu, wheelMenuRenderer_t* renderer, uint16_t angle, uint16_t radius) +{ + renderer->touched = true; + renderer->active = true; + + if (radius <= (renderer->centerR * 1024 / renderer->selR)) + { + if (!renderer->customBack && menu->parentMenu) + { + return menuNavigateToItem(menu, mnuBackStr); + } + else + { + if (menu->currentItem) + { + menu->currentItem = NULL; + menu->cbFunc(NULL, false, 0); + } + return menu; + } + } + + // Compensate for the "Back" item unless it's included in the ring + uint8_t ringItems = menu->items->length - ((menu->parentMenu && !renderer->customBack) ? 1 : 0); + uint16_t anchorAngle = (360 + renderer->anchorAngle - (360 / ringItems / 2)) % 360; + + // Check if the angle is actually before the starting angle + if ((angle % 360) < anchorAngle) + { + // just add 360 to it -- so if e.g. angle is 15 and startAngle is 20, angle + // would then be 375, and (375 - 20) = 355, so the offset is still < 360 + angle = (angle % 360) + 360; + } + + // Calculate the offset + uint8_t index = (angle - anchorAngle) * ringItems / 360; + + if (index < ringItems) + { + node_t* node = menu->items->first; + + // Find the item configured for that offset + while (node != NULL) + { + menuItem_t* menuItem = node->val; + wheelItemInfo_t* info = findInfo(renderer, menuItem->label ? menuItem->label : menuItem->options[0]); + if (info && (info->position == index)) + { + // Only navigate to it if it's not the current item already + if (node != menu->currentItem) + { + return menuNavigateToItem(menu, info->label); + } + + return menu; + } + + node = node->next; + } + } + + return menu; +} + +/** + * @brief TODO doc + * + * @param menu + * @param renderer + * @param evt + * @return menu_t* + */ +menu_t* wheelMenuButton(menu_t* menu, wheelMenuRenderer_t* renderer, const buttonEvt_t* evt) +{ + if (evt->down && menu->currentItem) + { + menuItem_t* item = menu->currentItem->val; + wheelItemInfo_t* info = findInfo(renderer, item->options ? item->options[0] : item->label); + + if (info && info->scroll != NO_SCROLL) + { + switch (evt->button) + { + case PB_UP: + { + if (info->scroll & SCROLL_VERT) + { + return (info->scroll & SCROLL_REVERSE) ? menuNavigateToPrevOption(menu) + : menuNavigateToNextOption(menu); + } + + break; + } + + case PB_DOWN: + { + if (info->scroll & SCROLL_VERT) + { + return (info->scroll & SCROLL_REVERSE) ? menuNavigateToNextOption(menu) + : menuNavigateToPrevOption(menu); + } + break; + } + + case PB_LEFT: + { + if (info->scroll & SCROLL_HORIZ) + { + return (info->scroll & SCROLL_REVERSE) ? menuNavigateToNextOption(menu) + : menuNavigateToPrevOption(menu); + } + break; + } + + case PB_RIGHT: + { + if (info->scroll & SCROLL_HORIZ) + { + return (info->scroll & SCROLL_REVERSE) ? menuNavigateToPrevOption(menu) + : menuNavigateToNextOption(menu); + } + break; + } + + case PB_A: + { + return menuSelectCurrentItem(menu); + } + + case PB_B: + case PB_SELECT: + case PB_START: + default: + break; + } + } + } + + return menu; +} + +/** + * @brief TODO doc + * + * @param menu + * @param renderer + * @return menu_t* + */ +menu_t* wheelMenuTouchRelease(menu_t* menu, wheelMenuRenderer_t* renderer) +{ + if (!renderer->touched) + { + return menu; + } + + renderer->touched = false; + + if (!renderer->active) + { + return menu; + } + + if (menu->currentItem) + { + if (!menuItemHasSubMenu(menu->currentItem->val) && !menuItemIsBack(menu->currentItem->val)) + { + // Don't stay active after the touch is released + renderer->active = false; + + menuSelectCurrentItem(menu); + + while (NULL != menu->parentMenu) + { + menu = menu->parentMenu; + } + + return menu; + } + + return menuSelectCurrentItem(menu); + } + else + { + // No selection! Go back + if (menu->parentMenu) + { + return menu->parentMenu; + } + else + { + // There's no higher level menu, just close + renderer->active = false; + return menu; + } + } +} + +/** + * @brief TODO doc + * + * @param menu + * @param renderer + * @return true + * @return false + */ +bool wheelMenuActive(menu_t* menu, wheelMenuRenderer_t* renderer) +{ + return renderer && renderer->active; +} + +/** + * @brief TODO doc + * + * @param renderer + * @param label + * @return wheelItemInfo_t* + */ +static wheelItemInfo_t* findInfo(wheelMenuRenderer_t* renderer, const char* label) +{ + node_t* node = renderer->itemInfos.first; + while (node != NULL) + { + wheelItemInfo_t* info = node->val; + + if (info->label == label) + { + return info; + } + + node = node->next; + } + + return NULL; +} diff --git a/main/utils/wheel_menu.h b/main/utils/wheel_menu.h new file mode 100644 index 000000000..295a0e49e --- /dev/null +++ b/main/utils/wheel_menu.h @@ -0,0 +1,84 @@ +/*! \file wheel_menu.h + * + * The new and improved paint menu will work like this, thanks to the new touchpad: + * + * 1. When the touchpad is pressed, the canvas is saved (if not already otherwise?) + * 2. A ring selector is drawn over the center of the screen. It is divided into 4 sectors, with + * a dead-zone circle in the center. When the touchpad is moved towards a ring, it is highlighted. + * 3. There are multiple types of entries in the ring. Some will move to another menu when selected + * by releasing the touchpad, and some will allow pressing up/down and/or left/right on the D-pad + * to change values. If a sub-menu is selected, it can be exited by pressing B, or maybe there + * will be an "exit" ring? + * 4. Maybe there shuold be an option to have a "sticky" menu -- instead of requiring you to hold + * the touchpad at the same time as going up/down, it would keep you in the menu until you hit B. + * 5. "Tool Ring" Layout: + * - The top sector is the tool selector, with left/right. + * - The left sector is the color selector, with up/down. And, maybe A or Left edits the color? + * - The right sector is the tool size selector, with left/right. + * - The bottom sector is the "..." or "Settings" menu + * 6. "Settings" layout: + * - Save + * - Load + * - New + * - Exit + * - ... edit palette? + */ +#ifndef _WHEEL_MENU_H_ +#define _WHEEL_MENU_H_ + +#include "menu.h" +#include "menu_utils.h" +#include "wsg.h" +#include "geometry.h" +#include "palette.h" + +/** + * @brief Wheel scroll directions + */ +typedef enum +{ + NO_SCROLL = 0, ///< TODO doc + SCROLL_VERT = 1, ///< TODO doc + SCROLL_HORIZ = 2, ///< TODO doc + SCROLL_REVERSE = 4, ///< TODO doc + SCROLL_VERT_R = SCROLL_VERT | SCROLL_REVERSE, ///< TODO doc + SCROLL_HORIZ_R = SCROLL_HORIZ | SCROLL_REVERSE, ///< TODO doc +} wheelScrollDir_t; + +/** + * @brief Renderer for a menu wheel + */ +typedef struct +{ + const font_t* font; ///< The font to draw the menu labels with + const rectangle_t* textBox; ///< A pointer to the text box to draw the selected item's label inside + list_t itemInfos; ///< The list holding each item's information + uint16_t anchorAngle; ///< The angle around which the 0th menu item will be centered + uint16_t x; ///< The X position of the center of the menu + uint16_t y; ///< The Y position of the center of the menu + uint16_t centerR; ///< The radius of the center circle of the menu, or 0 if none + uint16_t unselR; ///< The radius of unselected items' sectors + uint16_t selR; ///< The radius of the selected sector + paletteColor_t textColor; ///< TODO doc + paletteColor_t unselBgColor; ///< TODO doc + paletteColor_t selBgColor; ///< TODO doc + paletteColor_t borderColor; ///< TODO doc + bool customBack; ///< TODO doc + bool touched; ///< TODO doc + bool active; ///< TODO doc +} wheelMenuRenderer_t; + +wheelMenuRenderer_t* initWheelMenu(const font_t* font, uint16_t anchorAngle, const rectangle_t* textBox); +void deinitWheelMenu(wheelMenuRenderer_t* renderer); +void drawWheelMenu(menu_t* menu, wheelMenuRenderer_t* renderer, int64_t elapsedUs); + +void wheelMenuSetItemInfo(wheelMenuRenderer_t* renderer, const char* label, const wsg_t* icon, uint8_t position, + wheelScrollDir_t scrollDir); +void wheelMenuSetItemColor(wheelMenuRenderer_t* renderer, const char* label, paletteColor_t selectedBg, + paletteColor_t unselectedBg); +menu_t* wheelMenuTouch(menu_t* menu, wheelMenuRenderer_t* renderer, uint16_t angle, uint16_t radius); +menu_t* wheelMenuButton(menu_t* menu, wheelMenuRenderer_t* renderer, const buttonEvt_t* evt); +menu_t* wheelMenuTouchRelease(menu_t* menu, wheelMenuRenderer_t* renderer); +bool wheelMenuActive(menu_t* menu, wheelMenuRenderer_t* renderer); + +#endif diff --git a/makefile b/makefile index be86e185e..2f8638112 100644 --- a/makefile +++ b/makefile @@ -87,7 +87,14 @@ CFLAGS = \ ifeq ($(HOST_OS),Linux) CFLAGS += \ -fsanitize=address \ + -fsanitize=bounds-strict \ -fno-omit-frame-pointer + +ENABLE_GCOV=false + +ifeq ($(ENABLE_GCOV),true) + CFLAGS += -fprofile-arcs -ftest-coverage -DENABLE_GCOV +endif endif # These are warning flags that the IDF uses @@ -118,7 +125,7 @@ CFLAGS_WARNINGS_EXTRA = \ -Wshadow \ -Wredundant-decls \ -Wjump-misses-init \ - -Wswitch-enum \ + -Wswitch \ -Wcast-align \ -Wformat-nonliteral \ -Wno-switch-default \ @@ -165,7 +172,7 @@ DEFINES_LIST = \ CONFIG_NUM_LEDS=8 \ configENABLE_FREERTOS_DEBUG_OCDAWARE=1 \ _GNU_SOURCE \ - IDF_VER="v5.1" \ + IDF_VER="v5.1.1" \ ESP_PLATFORM \ _POSIX_READER_WRITER_LOCKS \ CFG_TUSB_MCU=OPT_MCU_ESP32S2 @@ -214,8 +221,13 @@ LIBRARY_FLAGS = $(patsubst %, -L%, $(LIB_DIRS)) $(patsubst %, -l%, $(LIBS)) \ ifeq ($(HOST_OS),Linux) LIBRARY_FLAGS += \ -fsanitize=address \ + -fsanitize=bounds-strict \ -fno-omit-frame-pointer \ -static-libasan + +ifeq ($(ENABLE_GCOV),true) + LIBRARY_FLAGS += -lgcov -fprofile-arcs -ftest-coverage +endif endif ################################################################################ @@ -319,7 +331,8 @@ CPPCHECK_FLAGS= \ --std=c++17 \ --suppress=missingIncludeSystem \ --output-file=./cppcheck_result.txt \ - -j12 + -j12 \ + -D__linux__=1 CPPCHECK_DIRS= \ main \ @@ -344,5 +357,10 @@ CPPCHECK_IGNORE_FLAGS = $(patsubst %,-i%, $(CPPCHECK_IGNORE)) cppcheck: cppcheck $(CPPCHECK_FLAGS) $(DEFINES) $(INC) $(CPPCHECK_DIRS) $(CPPCHECK_IGNORE_FLAGS) +gen-coverage: + lcov --capture --directory ./emulator/obj/ --output-file ./coverage.info + genhtml ./coverage.info --output-directory ./coverage + firefox ./coverage/index.html & + # Print any value from this makefile print-% : ; @echo $* = $($*) diff --git a/sdkconfig b/sdkconfig index 4eac04976..f02aa57e1 100644 --- a/sdkconfig +++ b/sdkconfig @@ -1,6 +1,6 @@ # # Automatically generated file. DO NOT EDIT. -# Espressif IoT Development Framework (ESP-IDF) Project Configuration +# Espressif IoT Development Framework (ESP-IDF) 5.1.1 Project Configuration # CONFIG_SOC_ADC_SUPPORTED=y CONFIG_SOC_DAC_SUPPORTED=y @@ -92,6 +92,7 @@ CONFIG_SOC_DEDIC_GPIO_HAS_INTERRUPT=y CONFIG_SOC_DEDIC_GPIO_OUT_AUTO_ENABLE=y CONFIG_SOC_I2C_NUM=2 CONFIG_SOC_I2C_FIFO_LEN=32 +CONFIG_SOC_I2C_CMD_REG_NUM=16 CONFIG_SOC_I2C_SUPPORT_SLAVE=y CONFIG_SOC_I2C_SUPPORT_HW_CLR_BUS=y CONFIG_SOC_I2C_SUPPORT_REF_TICK=y @@ -258,6 +259,7 @@ CONFIG_SOC_RTC_SLOW_CLK_SUPPORT_RC_FAST_D256=y CONFIG_SOC_CLK_RC_FAST_SUPPORT_CALIBRATION=y CONFIG_SOC_CLK_XTAL32K_SUPPORTED=y CONFIG_SOC_COEX_HW_PTI=y +CONFIG_SOC_EXTERNAL_COEX_LEADER_TX_LINE=y CONFIG_SOC_TEMPERATURE_SENSOR_SUPPORT_FAST_RC=y CONFIG_SOC_WIFI_HW_TSF=y CONFIG_SOC_WIFI_FTM_SUPPORT=y @@ -344,6 +346,7 @@ CONFIG_ESP_ROM_HAS_UART_BUF_SWITCH=y CONFIG_ESP_ROM_NEEDS_SWSETUP_WORKAROUND=y CONFIG_ESP_ROM_HAS_REGI2C_BUG=y CONFIG_ESP_ROM_HAS_NEWLIB_NANO_FORMAT=y +CONFIG_ESP_ROM_HAS_FLASH_COUNT_PAGES_BUG=y # # Boot ROM Behavior @@ -427,11 +430,11 @@ CONFIG_TFT_MAX_BRIGHTNESS=200 # CONFIG_COMPILER_OPTIMIZATION_SIZE is not set CONFIG_COMPILER_OPTIMIZATION_PERF=y # CONFIG_COMPILER_OPTIMIZATION_NONE is not set -CONFIG_COMPILER_OPTIMIZATION_ASSERTIONS_ENABLE=y +# CONFIG_COMPILER_OPTIMIZATION_ASSERTIONS_ENABLE is not set # CONFIG_COMPILER_OPTIMIZATION_ASSERTIONS_SILENT is not set -# CONFIG_COMPILER_OPTIMIZATION_ASSERTIONS_DISABLE is not set +CONFIG_COMPILER_OPTIMIZATION_ASSERTIONS_DISABLE=y CONFIG_COMPILER_FLOAT_LIB_FROM_GCCLIB=y -CONFIG_COMPILER_OPTIMIZATION_ASSERTION_LEVEL=2 +CONFIG_COMPILER_OPTIMIZATION_ASSERTION_LEVEL=0 # CONFIG_COMPILER_OPTIMIZATION_CHECKS_SILENT is not set CONFIG_COMPILER_HIDE_PATHS_MACROS=y # CONFIG_COMPILER_CXX_EXCEPTIONS is not set @@ -524,7 +527,7 @@ CONFIG_GPIO_CTRL_FUNC_IN_IRAM=y # GPTimer Configuration # CONFIG_GPTIMER_CTRL_FUNC_IN_IRAM=y -# CONFIG_GPTIMER_ISR_IRAM_SAFE is not set +CONFIG_GPTIMER_ISR_IRAM_SAFE=y # CONFIG_GPTIMER_SUPPRESS_DEPRECATE_WARN is not set # CONFIG_GPTIMER_ENABLE_DEBUG_LOG is not set # end of GPTimer Configuration @@ -576,7 +579,7 @@ CONFIG_EFUSE_MAX_BLK_LEN=256 # ESP-TLS # CONFIG_ESP_TLS_USING_MBEDTLS=y -# CONFIG_ESP_TLS_USE_DS_PERIPHERAL is not set +CONFIG_ESP_TLS_USE_DS_PERIPHERAL=y # CONFIG_ESP_TLS_PSK_VERIFICATION is not set # CONFIG_ESP_TLS_INSECURE is not set # end of ESP-TLS @@ -584,8 +587,8 @@ CONFIG_ESP_TLS_USING_MBEDTLS=y # # ADC and ADC Calibration # -# CONFIG_ADC_ONESHOT_CTRL_FUNC_IN_IRAM is not set -# CONFIG_ADC_CONTINUOUS_ISR_IRAM_SAFE is not set +CONFIG_ADC_ONESHOT_CTRL_FUNC_IN_IRAM=y +CONFIG_ADC_CONTINUOUS_ISR_IRAM_SAFE=y CONFIG_ADC_DISABLE_DAC_OUTPUT=y # end of ADC and ADC Calibration @@ -676,21 +679,20 @@ CONFIG_ESP_REV_MAX_FULL=199 # MAC Config # CONFIG_ESP_MAC_ADDR_UNIVERSE_WIFI_STA=y -CONFIG_ESP_MAC_ADDR_UNIVERSE_WIFI_AP=y -CONFIG_ESP_MAC_UNIVERSAL_MAC_ADDRESSES_TWO=y -# CONFIG_ESP32S2_UNIVERSAL_MAC_ADDRESSES_ONE is not set -CONFIG_ESP32S2_UNIVERSAL_MAC_ADDRESSES_TWO=y -CONFIG_ESP32S2_UNIVERSAL_MAC_ADDRESSES=2 +CONFIG_ESP_MAC_UNIVERSAL_MAC_ADDRESSES_ONE=y +CONFIG_ESP32S2_UNIVERSAL_MAC_ADDRESSES_ONE=y +# CONFIG_ESP32S2_UNIVERSAL_MAC_ADDRESSES_TWO is not set +CONFIG_ESP32S2_UNIVERSAL_MAC_ADDRESSES=1 # end of MAC Config # # Sleep Config # -CONFIG_ESP_SLEEP_RTC_BUS_ISO_WORKAROUND=y -# CONFIG_ESP_SLEEP_GPIO_RESET_WORKAROUND is not set -CONFIG_ESP_SLEEP_PSRAM_LEAKAGE_WORKAROUND=y CONFIG_ESP_SLEEP_FLASH_LEAKAGE_WORKAROUND=y +CONFIG_ESP_SLEEP_PSRAM_LEAKAGE_WORKAROUND=y # CONFIG_ESP_SLEEP_MSPI_NEED_ALL_IO_PU is not set +CONFIG_ESP_SLEEP_RTC_BUS_ISO_WORKAROUND=y +# CONFIG_ESP_SLEEP_GPIO_RESET_WORKAROUND is not set # end of Sleep Config # @@ -739,6 +741,7 @@ CONFIG_LCD_PANEL_IO_FORMAT_BUF_SIZE=32 CONFIG_ESP_NETIF_IP_LOST_TIMER_INTERVAL=120 # CONFIG_ESP_NETIF_TCPIP_LWIP is not set CONFIG_ESP_NETIF_LOOPBACK=y +# CONFIG_ESP_NETIF_RECEIVE_REPORT_ERRORS is not set # CONFIG_ESP_NETIF_L2_TAP is not set # end of ESP NETIF Adapter @@ -876,13 +879,8 @@ CONFIG_ESP_CONSOLE_UART=y CONFIG_ESP_CONSOLE_MULTIPLE_UART=y CONFIG_ESP_CONSOLE_UART_NUM=0 CONFIG_ESP_CONSOLE_UART_BAUDRATE=115200 -CONFIG_ESP_INT_WDT=y -CONFIG_ESP_INT_WDT_TIMEOUT_MS=300 -CONFIG_ESP_TASK_WDT_EN=y -CONFIG_ESP_TASK_WDT_INIT=y -# CONFIG_ESP_TASK_WDT_PANIC is not set -CONFIG_ESP_TASK_WDT_TIMEOUT_S=5 -CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU0=y +# CONFIG_ESP_INT_WDT is not set +# CONFIG_ESP_TASK_WDT_EN is not set # CONFIG_ESP_PANIC_HANDLER_IRAM is not set # CONFIG_ESP_DEBUG_STUBS_ENABLE is not set CONFIG_ESP_DEBUG_OCDAWARE=y @@ -954,7 +952,7 @@ CONFIG_ESP_WIFI_RX_IRAM_OPT=y # CONFIG_ESP_WIFI_ENABLE_WPA3_OWE_STA is not set # CONFIG_ESP_WIFI_SLP_IRAM_OPT is not set # CONFIG_ESP_WIFI_FTM_ENABLE is not set -# CONFIG_ESP_WIFI_STA_DISCONNECTED_PM_ENABLE is not set +CONFIG_ESP_WIFI_STA_DISCONNECTED_PM_ENABLE=y # CONFIG_ESP_WIFI_GMAC_SUPPORT is not set # CONFIG_ESP_WIFI_SOFTAP_SUPPORT is not set # CONFIG_ESP_WIFI_SLP_BEACON_LOST_OPT is not set @@ -1085,9 +1083,7 @@ CONFIG_FREERTOS_DEBUG_OCDAWARE=y # CONFIG_HAL_ASSERTION_EQUALS_SYSTEM=y # CONFIG_HAL_ASSERTION_DISABLE is not set -# CONFIG_HAL_ASSERTION_SILENT is not set -# CONFIG_HAL_ASSERTION_ENABLE is not set -CONFIG_HAL_DEFAULT_ASSERTION_LEVEL=2 +CONFIG_HAL_DEFAULT_ASSERTION_LEVEL=0 CONFIG_HAL_SPI_MASTER_FUNC_IN_IRAM=y # end of Hardware Abstraction Layer (HAL) and Low Level (LL) @@ -1122,7 +1118,7 @@ CONFIG_LOG_MAXIMUM_EQUALS_DEFAULT=y # CONFIG_LOG_MAXIMUM_LEVEL_DEBUG is not set # CONFIG_LOG_MAXIMUM_LEVEL_VERBOSE is not set CONFIG_LOG_MAXIMUM_LEVEL=3 -CONFIG_LOG_COLORS=y +# CONFIG_LOG_COLORS is not set CONFIG_LOG_TIMESTAMP_SOURCE_RTOS=y # CONFIG_LOG_TIMESTAMP_SOURCE_SYSTEM is not set # end of Log output @@ -1144,16 +1140,17 @@ CONFIG_LWIP_MAX_SOCKETS=10 # CONFIG_LWIP_SO_REUSE is not set # CONFIG_LWIP_SO_RCVBUF is not set # CONFIG_LWIP_NETBUF_RECVINFO is not set -# CONFIG_LWIP_IP4_FRAG is not set +CONFIG_LWIP_IP4_FRAG=y # CONFIG_LWIP_IP4_REASSEMBLY is not set CONFIG_LWIP_IP_REASS_MAX_PBUFS=10 # CONFIG_LWIP_IP_FORWARD is not set # CONFIG_LWIP_STATS is not set -# CONFIG_LWIP_ESP_GRATUITOUS_ARP is not set +CONFIG_LWIP_ESP_GRATUITOUS_ARP=y +CONFIG_LWIP_GARP_TMR_INTERVAL=60 CONFIG_LWIP_TCPIP_RECVMBOX_SIZE=32 -# CONFIG_LWIP_DHCP_DOES_ARP_CHECK is not set +CONFIG_LWIP_DHCP_DOES_ARP_CHECK=y # CONFIG_LWIP_DHCP_DISABLE_CLIENT_ID is not set -# CONFIG_LWIP_DHCP_DISABLE_VENDOR_CLASS_ID is not set +CONFIG_LWIP_DHCP_DISABLE_VENDOR_CLASS_ID=y # CONFIG_LWIP_DHCP_RESTORE_LAST_IP is not set CONFIG_LWIP_DHCP_OPTIONS_LEN=68 CONFIG_LWIP_NUM_NETIF_CLIENT_DATA=0 @@ -1169,7 +1166,8 @@ CONFIG_LWIP_DHCP_COARSE_TIMER_SECS=1 CONFIG_LWIP_IPV4=y # CONFIG_LWIP_IPV6 is not set # CONFIG_LWIP_NETIF_STATUS_CALLBACK is not set -# CONFIG_LWIP_NETIF_LOOPBACK is not set +CONFIG_LWIP_NETIF_LOOPBACK=y +CONFIG_LWIP_LOOPBACK_MAX_PBUFS=8 # # TCP @@ -1210,7 +1208,7 @@ CONFIG_LWIP_UDP_RECVMBOX_SIZE=6 # CONFIG_LWIP_CHECKSUM_CHECK_ICMP is not set # end of Checksums -CONFIG_LWIP_TCPIP_TASK_STACK_SIZE=2048 +CONFIG_LWIP_TCPIP_TASK_STACK_SIZE=3072 CONFIG_LWIP_TCPIP_TASK_AFFINITY_NO_AFFINITY=y # CONFIG_LWIP_TCPIP_TASK_AFFINITY_CPU0 is not set CONFIG_LWIP_TCPIP_TASK_AFFINITY=0x7FFFFFFF @@ -1238,7 +1236,6 @@ CONFIG_LWIP_SNTP_UPDATE_DELAY=3600000 # end of SNTP CONFIG_LWIP_BRIDGEIF_MAX_PORTS=7 -# CONFIG_LWIP_ESP_LWIP_ASSERT is not set # # Hooks @@ -1257,8 +1254,8 @@ CONFIG_LWIP_HOOK_NETCONN_EXT_RESOLVE_NONE=y # # mbedTLS # -CONFIG_MBEDTLS_INTERNAL_MEM_ALLOC=y -# CONFIG_MBEDTLS_EXTERNAL_MEM_ALLOC is not set +# CONFIG_MBEDTLS_INTERNAL_MEM_ALLOC is not set +CONFIG_MBEDTLS_EXTERNAL_MEM_ALLOC=y # CONFIG_MBEDTLS_DEFAULT_MEM_ALLOC is not set # CONFIG_MBEDTLS_CUSTOM_MEM_ALLOC is not set CONFIG_MBEDTLS_ASYMMETRIC_CONTENT_LEN=y @@ -1272,6 +1269,7 @@ CONFIG_MBEDTLS_SSL_OUT_CONTENT_LEN=4096 # # CONFIG_MBEDTLS_SSL_VARIABLE_BUFFER_LENGTH is not set # CONFIG_MBEDTLS_X509_TRUSTED_CERT_CALLBACK is not set +# CONFIG_MBEDTLS_SSL_CONTEXT_SERIALIZATION is not set # CONFIG_MBEDTLS_SSL_KEEP_PEER_CERTIFICATE is not set # end of mbedTLS v3.x related @@ -1282,12 +1280,15 @@ CONFIG_MBEDTLS_SSL_OUT_CONTENT_LEN=4096 # end of Certificate Bundle # CONFIG_MBEDTLS_CMAC_C is not set -# CONFIG_MBEDTLS_HARDWARE_AES is not set -# CONFIG_MBEDTLS_HARDWARE_MPI is not set -# CONFIG_MBEDTLS_HARDWARE_SHA is not set +CONFIG_MBEDTLS_HARDWARE_AES=y +CONFIG_MBEDTLS_AES_USE_INTERRUPT=y +CONFIG_MBEDTLS_HARDWARE_GCM=y +CONFIG_MBEDTLS_HARDWARE_MPI=y +CONFIG_MBEDTLS_MPI_USE_INTERRUPT=y +CONFIG_MBEDTLS_HARDWARE_SHA=y # CONFIG_MBEDTLS_ROM_MD5 is not set -# CONFIG_MBEDTLS_ATCA_HW_ECDSA_SIGN is not set -# CONFIG_MBEDTLS_ATCA_HW_ECDSA_VERIFY is not set +CONFIG_MBEDTLS_ATCA_HW_ECDSA_SIGN=y +CONFIG_MBEDTLS_ATCA_HW_ECDSA_VERIFY=y # CONFIG_MBEDTLS_HAVE_TIME is not set # CONFIG_MBEDTLS_ECDSA_DETERMINISTIC is not set # CONFIG_MBEDTLS_SHA512_C is not set @@ -1304,8 +1305,8 @@ CONFIG_MBEDTLS_AES_C=y # CONFIG_MBEDTLS_DES_C is not set # CONFIG_MBEDTLS_BLOWFISH_C is not set # CONFIG_MBEDTLS_XTEA_C is not set -# CONFIG_MBEDTLS_CCM_C is not set -# CONFIG_MBEDTLS_GCM_C is not set +CONFIG_MBEDTLS_CCM_C=y +CONFIG_MBEDTLS_GCM_C=y # CONFIG_MBEDTLS_NIST_KW_C is not set # end of Symmetric Ciphers @@ -1326,6 +1327,7 @@ CONFIG_MBEDTLS_AES_C=y # CONFIG_MBEDTLS_CHACHA20_C is not set # CONFIG_MBEDTLS_HKDF_C is not set # CONFIG_MBEDTLS_THREADING_C is not set +# CONFIG_MBEDTLS_LARGE_KEY_SOFTWARE_MPI is not set # CONFIG_MBEDTLS_SECURITY_RISKS is not set # end of mbedTLS @@ -1369,6 +1371,17 @@ CONFIG_NEWLIB_TIME_SYSCALL_USE_RTC_HRT=y # OpenThread # # CONFIG_OPENTHREAD_ENABLED is not set + +# +# Thread Operational Dataset +# +CONFIG_OPENTHREAD_NETWORK_NAME="OpenThread-ESP" +CONFIG_OPENTHREAD_NETWORK_CHANNEL=15 +CONFIG_OPENTHREAD_NETWORK_PANID=0x1234 +CONFIG_OPENTHREAD_NETWORK_EXTPANID="dead00beef00cafe" +CONFIG_OPENTHREAD_NETWORK_MASTERKEY="00112233445566778899aabbccddeeff" +CONFIG_OPENTHREAD_NETWORK_PSKC="104810e2315100afd6bc9215a6bfac53" +# end of Thread Operational Dataset # end of OpenThread # @@ -1493,17 +1506,7 @@ CONFIG_SPIFFS_USE_MTIME=y # # Ultra Low Power (ULP) Co-processor # -CONFIG_ULP_COPROC_ENABLED=y -# CONFIG_ULP_COPROC_TYPE_FSM is not set -CONFIG_ULP_COPROC_TYPE_RISCV=y -CONFIG_ULP_COPROC_RESERVE_MEM=4096 - -# -# ULP RISC-V Settings -# -CONFIG_ULP_RISCV_UART_BAUDRATE=9600 -CONFIG_ULP_RISCV_I2C_RW_TIMEOUT=500 -# end of ULP RISC-V Settings +# CONFIG_ULP_COPROC_ENABLED is not set # end of Ultra Low Power (ULP) Co-processor # @@ -1545,6 +1548,7 @@ CONFIG_VFS_SUPPORT_DIR=y CONFIG_VFS_SUPPORT_SELECT=y CONFIG_VFS_SUPPRESS_SELECT_DEBUG_OUTPUT=y CONFIG_VFS_SUPPORT_TERMIOS=y +CONFIG_VFS_MAX_COUNT=8 # # Host File System I/O (Semihosting) @@ -1566,7 +1570,7 @@ CONFIG_WL_SECTOR_SIZE=4096 # CONFIG_WIFI_PROV_SCAN_MAX_ENTRIES=16 CONFIG_WIFI_PROV_AUTOSTOP_TIMEOUT=30 -CONFIG_WIFI_PROV_BLE_FORCE_ENCRYPTION=y +# CONFIG_WIFI_PROV_BLE_FORCE_ENCRYPTION is not set CONFIG_WIFI_PROV_STA_ALL_CHANNEL_SCAN=y # CONFIG_WIFI_PROV_STA_FAST_SCAN is not set # end of Wi-Fi Provisioning Manager @@ -1675,10 +1679,10 @@ CONFIG_MONITOR_BAUD=115200 # CONFIG_COMPILER_OPTIMIZATION_LEVEL_DEBUG is not set # CONFIG_OPTIMIZATION_LEVEL_RELEASE is not set # CONFIG_COMPILER_OPTIMIZATION_LEVEL_RELEASE is not set -CONFIG_OPTIMIZATION_ASSERTIONS_ENABLED=y +# CONFIG_OPTIMIZATION_ASSERTIONS_ENABLED is not set # CONFIG_OPTIMIZATION_ASSERTIONS_SILENT is not set -# CONFIG_OPTIMIZATION_ASSERTIONS_DISABLED is not set -CONFIG_OPTIMIZATION_ASSERTION_LEVEL=2 +CONFIG_OPTIMIZATION_ASSERTIONS_DISABLED=y +CONFIG_OPTIMIZATION_ASSERTION_LEVEL=0 # CONFIG_CXX_EXCEPTIONS is not set CONFIG_STACK_CHECK_NONE=y # CONFIG_STACK_CHECK_NORM is not set @@ -1729,13 +1733,7 @@ CONFIG_CONSOLE_UART_DEFAULT=y CONFIG_CONSOLE_UART=y CONFIG_CONSOLE_UART_NUM=0 CONFIG_CONSOLE_UART_BAUDRATE=115200 -CONFIG_INT_WDT=y -CONFIG_INT_WDT_TIMEOUT_MS=300 -CONFIG_TASK_WDT=y -CONFIG_ESP_TASK_WDT=y -# CONFIG_TASK_WDT_PANIC is not set -CONFIG_TASK_WDT_TIMEOUT_S=5 -CONFIG_TASK_WDT_CHECK_IDLE_TASK_CPU0=y +# CONFIG_INT_WDT is not set # CONFIG_ESP32_DEBUG_STUBS_ENABLE is not set CONFIG_ESP32S2_DEBUG_OCDAWARE=y CONFIG_BROWNOUT_DET=y @@ -1798,9 +1796,9 @@ CONFIG_TIMER_TASK_PRIORITY=1 CONFIG_TIMER_TASK_STACK_DEPTH=2048 CONFIG_TIMER_QUEUE_LENGTH=10 # CONFIG_ENABLE_STATIC_TASK_CLEAN_UP_HOOK is not set -# CONFIG_HAL_ASSERTION_SILIENT is not set # CONFIG_L2_TO_L3_COPY is not set -# CONFIG_ESP_GRATUITOUS_ARP is not set +CONFIG_ESP_GRATUITOUS_ARP=y +CONFIG_GARP_TMR_INTERVAL=60 CONFIG_TCPIP_RECVMBOX_SIZE=32 CONFIG_TCP_MAXRTX=12 CONFIG_TCP_SYNMAXRTX=12 @@ -1814,7 +1812,7 @@ CONFIG_TCP_OVERSIZE_MSS=y # CONFIG_TCP_OVERSIZE_QUARTER_MSS is not set # CONFIG_TCP_OVERSIZE_DISABLE is not set CONFIG_UDP_RECVMBOX_SIZE=6 -CONFIG_TCPIP_TASK_STACK_SIZE=2048 +CONFIG_TCPIP_TASK_STACK_SIZE=3072 CONFIG_TCPIP_TASK_AFFINITY_NO_AFFINITY=y # CONFIG_TCPIP_TASK_AFFINITY_CPU0 is not set CONFIG_TCPIP_TASK_AFFINITY=0x7FFFFFFF @@ -1833,9 +1831,7 @@ CONFIG_ESP32_PTHREAD_TASK_NAME_DEFAULT="pthread" CONFIG_SPI_FLASH_WRITING_DANGEROUS_REGIONS_ABORTS=y # CONFIG_SPI_FLASH_WRITING_DANGEROUS_REGIONS_FAILS is not set # CONFIG_SPI_FLASH_WRITING_DANGEROUS_REGIONS_ALLOWED is not set -CONFIG_ESP32S2_ULP_COPROC_ENABLED=y -CONFIG_ESP32S2_ULP_COPROC_RISCV=y -CONFIG_ESP32S2_ULP_COPROC_RESERVE_MEM=4096 +# CONFIG_ESP32S2_ULP_COPROC_ENABLED is not set CONFIG_SUPPRESS_SELECT_DEBUG_OUTPUT=y CONFIG_SUPPORT_TERMIOS=y CONFIG_SEMIHOSTFS_MAX_MOUNT_POINTS=1 diff --git a/tools/README.md b/tools/README.md index 9975c73fa..9e543c11b 100644 --- a/tools/README.md +++ b/tools/README.md @@ -13,4 +13,8 @@ This file can be given to `spiffs_file_preprocessor` to flash to the Swadge and ## [`pyFlashGui`](./pyFlashGui) -`pyFlashGui` is a Python GUI program which is used to program Swadges during manufacturing. It spins around and programs Swadges as they are connected to the host computer over USB. \ No newline at end of file +`pyFlashGui` is a Python GUI program which is used to program Swadges during manufacturing. It spins around and programs Swadges as they are connected to the host computer over USB. + +## `monitor_emu_wifi.py` + +`monitor_emu_wifi.py` is a Python command-line program which listens for emulated ESPNOW packets and prints them for debugging purposes. diff --git a/tools/breakout_editor/bombtest.tmx b/tools/breakout_editor/bombtest.tmx new file mode 100644 index 000000000..b992569e1 --- /dev/null +++ b/tools/breakout_editor/bombtest.tmx @@ -0,0 +1,41 @@ + + + + + + + + +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,2,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,1,130,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,6,6,6,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,6,19,6,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,6,6,6,0,0,0,6,6,6,0,0,0,6,6,6,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,6,18,6,0,0,0,0,0,0,0,0,0,6,20,6,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,6,6,6,0,0,0,0,0,0,0,0,0,6,6,6,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,6,6,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,6,6,6,0,0,0,4,0,0, +0,0,4,0,0,0,6,17,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,6,21,6,0,0,0,4,0,0, +0,0,4,0,0,0,6,6,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,6,6,6,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,145,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,1,129,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0 + + + diff --git a/tools/breakout_editor/breakout-editor.js b/tools/breakout_editor/breakout-editor.js new file mode 100644 index 000000000..3f6143bc8 --- /dev/null +++ b/tools/breakout_editor/breakout-editor.js @@ -0,0 +1,111 @@ +tiled.registerMapFormat("Breakout", { + name: "Breakout map format", + extension: "bin", + read: (fileName) => { + var file = new BinaryFile(fileName, BinaryFile.ReadOnly); + var filePath = FileInfo.path(fileName); + var buffer = file.read(file.size); + var view = new Uint8Array(buffer); + const tileDataOffset = 2; + const tileSizeInPixels = 16; + + var map = new TileMap(); + + //The first two bytes contain the width and height of the tilemap in tiles + var tileDataLength = view[0] * view[1]; + map.setSize(view[0], view[1]); + map.setTileSize(tileSizeInPixels, tileSizeInPixels); + + var tileset = tiled.open(filePath + '/breakout-tiles.tsx'); + + var layer = new TileLayer(); + + map.addTileset(tileset); + + layer.width = map.width; + layer.height = map.height; + layer.name = 'Main'; + + var layerEdit = layer.edit(); + var importTileX = 0; + var importTileY = 0; + + + //Import tile data + for(let i = 0; i < tileDataLength; i++){ + let tileId = view[i + tileDataOffset]; + layerEdit.setTile(importTileX, importTileY, tileset.tile(tileId)); + + importTileX++; + if(importTileX >= map.width){ + importTileY++; + importTileX=0; + } + } + + layerEdit.apply(); + + map.addLayer(layer); + file.close(); + return map; + }, + write: (map, fileName) => { + for (let i = 0; i < map.layerCount; ++i) { + const layer = map.layerAt(i); + + if (!layer.isTileLayer) { + continue; + } + + let file = new BinaryFile(fileName, BinaryFile.WriteOnly); + let buffer = new ArrayBuffer(2 + layer.width * layer.height + 2); //Buffer sized to lenth byte + width byte + length * width of level bytes + 2 total target tile bytes + let view = new Uint8Array(buffer); + + //The first two bytes contain the width and height of the tilemap in tiles + view[0]=layer.width; + view[1]=layer.height; + let writePosition = 2; + let totalTargetBlockTiles = 0; + + for (let y = 0; y < layer.height; ++y) { + const row = []; + + for (let x = 0; x < layer.width; ++x) { + const tile = layer.tileAt(x, y); + if(!tile){ + //file.write(0); + view[writePosition] = 0; + writePosition++; + continue; + } + + const tileId = tile.id; + + //Handle "target block tiles" + //These are the blocks that the player must break to complete the level. + if(tileId >= 16 && tileId <= 127) { + totalTargetBlockTiles++; + } + + //Handling every tile + view[writePosition]=tileId; + + writePosition++; + } + } + + //The last 2 bytes hold the total number of "target block tiles" + //Forced into a (hopefully) unsigned 16 bit integer, little endian + //There's probably a better way to do this... + let totalTargetBlockTilesLowerByte = totalTargetBlockTiles & 255; + let totalTargetBlockTilesUpperByte = (totalTargetBlockTiles >> 8) & 255; + view[writePosition] = totalTargetBlockTilesLowerByte; + writePosition++; + view[writePosition] = totalTargetBlockTilesUpperByte; + writePosition++; + + file.write(buffer); + file.commit(); + } + }, +}); \ No newline at end of file diff --git a/tools/breakout_editor/breakout-tiles.png b/tools/breakout_editor/breakout-tiles.png new file mode 100644 index 000000000..0f6d6c8d9 Binary files /dev/null and b/tools/breakout_editor/breakout-tiles.png differ diff --git a/tools/breakout_editor/breakout-tiles.tsx b/tools/breakout_editor/breakout-tiles.tsx new file mode 100644 index 000000000..1103f3506 --- /dev/null +++ b/tools/breakout_editor/breakout-tiles.tsx @@ -0,0 +1,4 @@ + + + + diff --git a/tools/breakout_editor/breakout.tiled-project b/tools/breakout_editor/breakout.tiled-project new file mode 100644 index 000000000..d58954aef --- /dev/null +++ b/tools/breakout_editor/breakout.tiled-project @@ -0,0 +1,11 @@ +{ + "automappingRulesFile": "", + "commands": [ + ], + "extensionsPath": "extensions", + "folders": [ + "." + ], + "propertyTypes": [ + ] +} diff --git a/tools/breakout_editor/breakout.tiled-session b/tools/breakout_editor/breakout.tiled-session new file mode 100644 index 000000000..7ce16e584 --- /dev/null +++ b/tools/breakout_editor/breakout.tiled-session @@ -0,0 +1,124 @@ +{ + "Map/SizeTest": { + "height": 4300, + "width": 2 + }, + "activeFile": "ponglike.tmx", + "expandedProjectPaths": [ + ], + "file.lastUsedOpenFilter": "All Files (*)", + "fileStates": { + "breakout-tiles.tsx": { + "dynamicWrapping": false, + "scaleInDock": 6, + "scaleInEditor": 6.04 + }, + "intro.tmx": { + "scale": 3.05, + "selectedLayer": 0, + "viewCenter": { + "x": 140.1639344262295, + "y": 118.19672131147543 + } + }, + "leftside.tmx": { + "scale": 3.0393238434163696, + "selectedLayer": 0, + "viewCenter": { + "x": 140.6562847608454, + "y": 120.58603126280666 + } + }, + "level1.tmx": { + "scale": 1.8793, + "selectedLayer": 0, + "viewCenter": { + "x": 131.1658596285851, + "y": 142.87234608630874 + } + }, + "mag01.tmx": { + "scale": 3.0501785714285714, + "selectedLayer": 0, + "viewCenter": { + "x": 140.15572858731926, + "y": 121.14044845149584 + } + }, + "ponglike.tmx": { + "scale": 2.865939597315436, + "selectedLayer": 0, + "viewCenter": { + "x": 131.37052865757275, + "y": 117.76242608746561 + } + }, + "split.tmx": { + "scale": 3.0501785714285714, + "selectedLayer": 0, + "viewCenter": { + "x": 140.15572858731926, + "y": 121.14044845149584 + } + }, + "starlite.tmx": { + "scale": 3.0501785714285714, + "selectedLayer": 0, + "viewCenter": { + "x": 140.48357824483344, + "y": 121.46829810901001 + } + }, + "swdglnd-character.tmx": { + "scale": 2.415, + "selectedLayer": 0, + "viewCenter": { + "x": 140.1656314699793, + "y": 120.28985507246375 + } + }, + "test.tmx": { + "scale": 3.0501785714285714, + "selectedLayer": 0, + "viewCenter": { + "x": 140.15572858731926, + "y": 120.15689947895325 + } + } + }, + "last.exportedFilePath": "/Users/jvega/Desktop", + "last.imagePath": "/Users/jvega/Pictures/My Art/Aseprite/breakout", + "map.height": 30, + "map.lastUsedExportFilter": "Breakout map format (*.bin)", + "map.lastUsedFormat": "tmx", + "map.tileHeight": 8, + "map.tileWidth": 8, + "map.width": 35, + "openFiles": [ + "intro.tmx", + "swdglnd-character.tmx", + "leftside.tmx", + "mag01.tmx", + "split.tmx", + "starlite.tmx", + "ponglike.tmx" + ], + "project": "breakout.tiled-project", + "recentFiles": [ + "intro.tmx", + "swdglnd-character.tmx", + "leftside.tmx", + "mag01.tmx", + "split.tmx", + "starlite.tmx", + "ponglike.tmx", + "test.tmx", + "level1.tmx", + "breakout-tiles.tsx" + ], + "tileset.lastUsedFormat": "tsx", + "tileset.tileSize": { + "height": 8, + "width": 8 + } +} diff --git a/tools/breakout_editor/intro.tmx b/tools/breakout_editor/intro.tmx new file mode 100644 index 000000000..2442d925b --- /dev/null +++ b/tools/breakout_editor/intro.tmx @@ -0,0 +1,41 @@ + + + + + + + + +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,2,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,37,38,37,38,37,38,37,38,37,38,37,38,37,38,37,38,37,38,37,38,37,38,37,38,0,0,0,4,0,0, +0,0,4,0,0,0,37,38,37,38,37,38,37,38,37,38,37,38,37,38,37,38,37,38,37,38,37,38,37,38,0,0,4,0,0, +0,0,4,0,0,35,36,35,36,35,36,35,36,35,36,1,1,1,1,35,36,35,36,35,36,35,36,35,36,0,0,0,4,0,0, +0,0,4,0,0,0,35,36,35,36,35,36,35,36,35,36,1,1,35,36,35,36,35,36,35,36,35,36,35,36,0,0,4,0,0, +0,0,4,0,0,33,34,33,34,33,34,33,34,33,34,1,1,1,1,33,34,33,34,33,34,33,34,33,34,0,0,0,4,0,0, +0,0,4,0,0,0,33,34,33,34,33,34,33,34,33,34,33,34,33,34,33,34,33,34,33,34,33,34,33,34,0,0,4,0,0, +0,0,4,0,0,33,34,33,34,33,34,33,34,33,34,33,34,33,34,33,34,33,34,33,34,33,34,33,34,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,145,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,1,129,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0 + + + diff --git a/tools/breakout_editor/leftside.tmx b/tools/breakout_editor/leftside.tmx new file mode 100644 index 000000000..d75ecabcf --- /dev/null +++ b/tools/breakout_editor/leftside.tmx @@ -0,0 +1,41 @@ + + + + + + + + +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,2,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,73,74,75,75,73,74,75,75,73,74,0,0,4,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,89,90,91,91,89,90,91,91,89,90,0,0,4,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,75,75,73,74,75,75,73,74,75,75,0,0,4,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,91,91,89,90,91,91,89,90,91,91,0,0,4,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,73,74,75,75,73,74,75,75,73,74,0,0,4,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,89,90,91,91,89,90,91,91,89,90,0,0,4,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,75,75,73,74,75,75,73,74,75,75,0,0,4,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,91,91,89,90,91,91,89,90,91,91,0,0,4,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,73,74,22,22,73,74,22,22,73,74,0,0,4,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,89,90,22,22,89,90,22,22,89,90,0,0,4,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,75,75,73,74,22,22,73,74,75,75,0,0,4,0,0, +0,0,0,131,145,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,91,91,89,90,22,22,89,90,91,91,1,1,4,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,73,74,22,22,73,74,22,22,73,74,0,0,4,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,89,90,22,22,89,90,22,22,89,90,0,0,4,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,75,75,73,74,75,75,73,74,75,75,0,0,4,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,91,91,89,90,91,91,89,90,91,91,0,0,4,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,73,74,75,75,73,74,75,75,73,74,0,0,4,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,89,90,91,91,89,90,91,91,89,90,0,0,4,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,75,75,73,74,75,75,73,74,75,75,0,0,4,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,91,91,89,90,91,91,89,90,91,91,0,0,4,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,73,74,75,75,73,74,75,75,73,74,0,0,4,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,89,90,91,91,89,90,91,91,89,90,0,0,4,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,2,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,0,0 + + + diff --git a/tools/breakout_editor/level1.tmx b/tools/breakout_editor/level1.tmx new file mode 100644 index 000000000..07bf896c3 --- /dev/null +++ b/tools/breakout_editor/level1.tmx @@ -0,0 +1,41 @@ + + + + + + + + +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,2,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,130,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,33,34,33,34,33,34,33,34,33,34,33,34,33,34,33,34,33,34,33,34,33,34,33,34,0,0,0,4,0,0, +0,0,4,0,0,0,33,19,19,19,33,34,33,34,33,20,20,20,33,34,33,34,33,25,33,34,33,34,33,34,0,0,4,0,0, +0,0,4,0,0,33,34,33,19,33,34,21,21,21,34,20,34,33,34,24,24,24,34,25,34,25,34,33,34,0,0,0,4,0,0, +0,0,4,0,0,0,33,34,19,34,33,21,33,34,33,20,20,20,33,34,24,34,33,25,33,25,33,34,33,34,0,0,4,0,0, +0,0,4,0,0,33,34,33,19,33,34,21,21,33,34,33,34,20,34,33,24,33,34,33,34,25,34,33,34,0,0,0,4,0,0, +0,0,4,0,0,0,33,34,19,34,33,21,33,34,33,20,20,20,33,34,24,34,33,25,33,34,33,34,33,34,0,0,4,0,0, +0,0,4,0,0,33,34,33,34,33,34,21,21,21,34,33,34,33,34,33,24,33,34,33,34,25,34,33,34,0,0,0,4,0,0, +0,0,4,0,0,0,33,34,33,34,33,34,33,34,33,34,33,34,33,34,33,34,33,34,33,34,33,34,33,34,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,145,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,129,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0 + + + diff --git a/tools/breakout_editor/mag01.tmx b/tools/breakout_editor/mag01.tmx new file mode 100644 index 000000000..b328019ee --- /dev/null +++ b/tools/breakout_editor/mag01.tmx @@ -0,0 +1,41 @@ + + + + + + + + +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,2,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,49,50,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,49,50,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,49,50,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,49,50,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,49,50,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,49,50,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,49,50,49,50,49,50,0,49,25,50,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,49,50,49,50,49,50,49,50,49,50,49,50,49,50,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,97,98,97,98,97,98,97,98,97,98,97,98,97,98,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,113,114,113,114,113,114,113,114,113,114,113,114,113,114,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,97,98,97,98,97,98,97,98,97,98,97,98,97,98,0,0,0,0,0,0,0,0,0,0,0,0,132,4,0,0, +0,0,4,0,0,113,114,113,114,113,114,113,114,113,114,113,114,113,114,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,145,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,129,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0 + + + diff --git a/tools/breakout_editor/mag02.tmx b/tools/breakout_editor/mag02.tmx new file mode 100644 index 000000000..5630d190c --- /dev/null +++ b/tools/breakout_editor/mag02.tmx @@ -0,0 +1,41 @@ + + + + + + + + +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,2,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,25,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,49,50,0,0,0,0,25,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,25,49,50,0,0,25,0,0,25,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,49,50,49,50,0,0,0,25,0,0,25,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,49,50,49,50,0,25,0,0,25,0,25,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,49,50,0,49,50,49,50,0,0,25,0,25,0,25,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,49,50,0,49,50,49,50,0,0,25,0,25,0,25,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,49,50,0,49,50,49,50,0,0,25,0,25,0,25,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,49,50,0,49,50,49,50,0,0,25,0,25,0,25,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,49,50,49,50,0,25,0,0,25,0,25,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,49,50,49,50,0,0,0,25,0,0,25,0,0,0,4,0,0, +0,0,4,131,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,25,49,50,0,0,25,0,0,25,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,49,50,0,0,0,0,25,0,0,0,0,1,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,25,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,145,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,129,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0 + + + diff --git a/tools/breakout_editor/ponglike.tmx b/tools/breakout_editor/ponglike.tmx new file mode 100644 index 000000000..778e85cca --- /dev/null +++ b/tools/breakout_editor/ponglike.tmx @@ -0,0 +1,41 @@ + + + + + + + + +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +1,1,2,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,1,1, +0,0,65,66,65,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,76,75,76,0,0, +0,0,81,82,81,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,92,91,92,0,0, +0,0,65,66,65,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,76,75,76,0,0, +1,1,81,82,81,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,92,91,92,1,1, +0,0,65,66,65,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,76,75,76,0,0, +0,0,81,82,81,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,92,91,92,0,0, +0,0,65,66,65,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,76,75,76,0,0, +0,0,81,82,81,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,92,91,92,0,0, +0,0,65,66,65,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,76,75,76,0,0, +0,0,81,82,81,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,92,91,92,0,0, +0,0,65,66,65,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,76,75,76,0,0, +0,0,81,82,81,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,92,91,92,0,0, +0,0,65,66,65,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,76,75,76,0,0, +0,0,81,82,81,131,145,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,132,92,91,92,0,0, +0,0,65,66,65,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,76,75,76,0,0, +0,0,81,82,81,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,92,91,92,0,0, +0,0,65,66,65,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,76,75,76,0,0, +0,0,81,82,81,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,92,91,92,0,0, +0,0,65,66,65,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,76,75,76,0,0, +0,0,81,82,81,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,92,91,92,0,0, +0,0,65,66,65,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,76,75,76,0,0, +0,0,81,82,81,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,92,91,92,0,0, +1,1,65,66,65,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,76,75,76,1,1, +0,0,81,82,81,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,92,91,92,0,0, +0,0,65,66,65,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,76,75,76,0,0, +0,0,81,82,81,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,92,91,92,0,0, +1,1,2,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,1,1 + + + diff --git a/tools/breakout_editor/rightside.tmx b/tools/breakout_editor/rightside.tmx new file mode 100644 index 000000000..7c320584d --- /dev/null +++ b/tools/breakout_editor/rightside.tmx @@ -0,0 +1,41 @@ + + + + + + + + +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,2,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,4,0,0,69,70,71,71,69,70,71,71,69,70,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,4,0,0,85,86,87,87,85,86,87,87,85,86,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,4,0,0,71,71,69,70,71,71,69,70,71,71,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,4,0,0,87,87,85,86,87,87,85,86,87,87,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,4,0,0,69,70,71,71,69,70,71,71,69,70,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,4,0,0,85,86,87,87,85,86,87,87,85,86,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,4,0,0,71,71,69,70,71,71,69,70,71,71,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,4,0,0,87,87,85,86,87,87,85,86,87,87,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,4,0,0,69,70,20,20,69,70,20,20,69,70,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,4,0,0,85,86,20,20,85,86,20,20,85,86,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,4,0,0,71,71,69,70,20,20,69,70,71,71,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,4,0,0,87,87,85,86,20,20,85,86,87,87,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,145,132,0,0,0, +0,0,4,0,0,69,70,20,20,69,70,20,20,69,70,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,4,0,0,85,86,20,20,85,86,20,20,85,86,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,4,0,0,71,71,69,70,71,71,69,70,71,71,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,4,0,0,87,87,85,86,87,87,85,86,87,87,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,4,0,0,69,70,71,71,69,70,71,71,69,70,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,4,0,0,85,86,87,87,85,86,87,87,85,86,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,4,0,0,71,71,69,70,71,71,69,70,71,71,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,4,0,0,87,87,85,86,87,87,85,86,87,87,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,4,0,0,69,70,71,71,69,70,71,71,69,70,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,4,0,0,85,86,87,87,85,86,87,87,85,86,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,2,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,0,0 + + + diff --git a/tools/breakout_editor/split.tmx b/tools/breakout_editor/split.tmx new file mode 100644 index 000000000..6dde459f1 --- /dev/null +++ b/tools/breakout_editor/split.tmx @@ -0,0 +1,41 @@ + + + + + + + + +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,2,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,130,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,6,7,6,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,35,36,35,36,35,36,35,36,35,36,35,36,8,0,8,41,42,41,42,41,42,41,42,41,42,41,42,0,4,0,0, +0,0,4,0,35,36,35,36,35,39,40,36,35,36,35,36,8,0,8,41,42,41,42,41,33,34,42,41,42,41,42,0,4,0,0, +0,0,4,0,35,36,35,36,39,40,39,40,35,36,35,36,8,0,8,41,42,41,42,41,33,34,42,41,42,41,42,0,4,0,0, +0,0,4,0,35,36,35,39,40,39,40,39,40,36,35,36,8,0,8,41,42,33,34,41,33,34,42,33,34,41,42,0,4,0,0, +0,0,4,0,35,36,39,40,35,39,40,36,39,40,35,36,8,0,8,41,42,41,33,34,33,34,33,34,42,41,42,0,4,0,0, +0,0,4,0,35,36,35,36,35,39,40,36,35,36,35,36,8,0,8,41,42,41,42,33,34,33,34,41,42,41,42,0,4,0,0, +0,0,4,0,35,36,35,36,35,39,40,36,35,36,35,36,8,0,8,41,42,41,42,41,33,34,42,41,42,41,42,0,4,0,0, +0,0,4,0,35,36,35,36,35,36,35,36,35,36,35,36,8,0,8,41,42,41,42,41,42,41,42,41,42,41,42,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,6,7,6,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,145,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,129,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0 + + + diff --git a/tools/breakout_editor/starlite.tmx b/tools/breakout_editor/starlite.tmx new file mode 100644 index 000000000..8c0160fc4 --- /dev/null +++ b/tools/breakout_editor/starlite.tmx @@ -0,0 +1,41 @@ + + + + + + + + +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,130,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,98,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,114,0,0,97,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,49,50,0,0,25,0,0,113,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,25,0,25,0,25,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,49,50,25,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,49,50,49,50,97,98,25,97,98,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,131,1,0,0,0,0,0,0,0,0,0,0,113,114,25,113,114,49,50,49,50,0,0,0,0,0,0,0,132,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,25,49,50,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,25,0,25,0,25,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,98,0,0,25,0,0,49,50,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,114,0,0,97,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,113,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,145,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,129,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0 + + + diff --git a/tools/breakout_editor/superhard.tmx b/tools/breakout_editor/superhard.tmx new file mode 100644 index 000000000..0478ef459 --- /dev/null +++ b/tools/breakout_editor/superhard.tmx @@ -0,0 +1,41 @@ + + + + + + + + +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,2,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,0,0, +0,0,4,0,0,0,0,0,0,0,0,6,0,0,0,0,130,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,6,0,0,0,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,6,0,0,0,6,0,0,0,6,6,6,6,6,6,6,6,6,6,6,6,6,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,6,0,0,0,6,0,0,0,6,0,0,0,0,0,0,0,0,0,0,0,6,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,6,0,0,0,6,0,0,0,6,0,0,0,0,0,0,0,0,0,0,0,6,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,6,0,0,0,6,0,0,0,6,0,1,17,0,0,0,0,0,0,0,0,6,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,6,0,0,0,6,0,0,0,6,0,1,1,1,6,6,6,6,0,0,0,6,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,6,0,0,0,6,0,0,0,6,0,0,0,1,6,6,6,6,0,0,0,6,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,6,0,0,0,6,0,0,0,6,6,6,6,6,6,6,6,6,0,0,0,6,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,6,0,0,0,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,6,0,0,0,0,4,0,0, +0,0,4,131,0,0,0,6,0,0,0,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,6,0,0,0,132,4,0,0, +0,0,4,0,0,0,0,6,0,0,0,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,6,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,6,0,0,0,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,6,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,6,0,0,0,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,145,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,129,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0 + + + diff --git a/tools/breakout_editor/swdglnd-character.tmx b/tools/breakout_editor/swdglnd-character.tmx new file mode 100644 index 000000000..9f518909c --- /dev/null +++ b/tools/breakout_editor/swdglnd-character.tmx @@ -0,0 +1,41 @@ + + + + + + + + +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,2,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,130,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,23,45,46,45,46,45,46,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,77,78,77,78,77,78,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,23,93,94,93,94,93,94,23,0,77,78,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,23,71,72,23,26,20,26,0,0,93,94,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,23,87,88,51,52,23,26,0,0,99,100,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,23,99,100,26,99,100,51,52,115,116,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,115,116,23,115,116,20,51,52,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,20,71,72,51,52,39,40,20,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,39,40,87,88,23,77,78,20,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,99,100,26,20,45,46,93,94,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,115,116,0,0,77,78,23,39,40,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,77,78,0,23,93,94,71,72,71,72,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,93,94,0,0,39,40,87,88,87,88,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,71,72,39,40,0,45,46,23,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,20,87,88,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,45,46,23,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,145,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,129,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0 + + + diff --git a/tools/breakout_editor/upsidedown.tmx b/tools/breakout_editor/upsidedown.tmx new file mode 100644 index 000000000..5ff16db19 --- /dev/null +++ b/tools/breakout_editor/upsidedown.tmx @@ -0,0 +1,41 @@ + + + + + + + + +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,1,130,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,145,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,21,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,75,76,21,75,76,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,75,76,91,92,21,91,92,75,76,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,75,76,91,92,75,76,21,75,76,91,92,75,76,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,77,78,91,92,75,76,91,92,21,91,92,75,76,91,92,77,78,0,0,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,47,48,93,94,75,76,91,92,75,76,21,75,76,91,92,75,76,93,94,47,48,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,47,48,77,78,91,92,75,76,91,92,0,91,92,75,76,91,92,77,78,47,48,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,47,48,93,94,75,76,91,92,0,0,0,0,0,91,92,75,76,93,94,47,48,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,47,48,77,78,91,92,0,0,0,0,0,0,0,0,0,91,92,77,78,47,48,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,47,48,93,94,0,0,0,0,0,0,0,0,0,0,0,0,0,93,94,47,48,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,47,48,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,47,48,0,0,0,0,4,0,0, +0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0, +0,0,4,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,4,0,0 + + + diff --git a/tools/monitor_emu_wifi.py b/tools/monitor_emu_wifi.py new file mode 100755 index 000000000..4a4e4f2c4 --- /dev/null +++ b/tools/monitor_emu_wifi.py @@ -0,0 +1,102 @@ +#!/usr/bin/env python +import socket +import string +import struct +import textwrap + +def hexdump(data: bytes): + return " ".join(["{:02X}".format(b) for b in data]) + +def pretty_macstr(mac: bytes): + """Converts a 12-byte MAC into a pretty string representation""" + return ':'.join((mac.decode("ASCII")[i:i+2] for i in range(0, 12, 2))) + +def pretty_macbin(mac: bytes): + """Returns the """ + return ':'.join((f"{b:02X}" for b in mac)) + +# The ESPNOW header, before the MAC string +ESPNOW_HEADER = b"ESP_NOW-" + +# Message types from p2pConnection.h +MSG_CONNECT = 0x00 +MSG_START = 0x01 +MSG_ACK = 0x02 +MSG_DATA_ACK = 0x03 +MSG_DATA = 0x04 + +# The types mapped onto strings for convenience +MSG_TYPE_NAMES = { + MSG_CONNECT: "CONNECT", + MSG_START: "START", + MSG_ACK: "ACK", + MSG_DATA_ACK: "ACK+D", + MSG_DATA: "DATA", +} + +INDENT = " "*32 + +def dbg(*args, **kwargs): + if debug: + print(*args, **kwargs) + +def handle_msg(addr, message): + if message.startswith(ESPNOW_HEADER): + (from_mac,) = struct.unpack_from("!12s", message, len(ESPNOW_HEADER)) + + line = f"{pretty_macstr(from_mac)} > " + + rest = message[len(ESPNOW_HEADER) + struct.calcsize("!12s") + 1:] + + if rest[0] == ord('p'): + # P2P message + line += "P2P " + + mode_id, msg_type = struct.unpack_from("!BB", rest, 1) + + type_str = MSG_TYPE_NAMES.get(msg_type, f"{msg_type:02X}") + + # Print the mode as a char if it's printable, or as hex otherwise + if chr(mode_id) in string.printable: + line += f"{chr(mode_id):2} " + else: + line += f"{mode_id:02X} " + + line += f"{type_str:<5}" + + # These all have a seqnum and dest mac + if msg_type in (MSG_START, MSG_ACK, MSG_DATA, MSG_DATA_ACK): + seqnum, to_mac = struct.unpack_from("!B6s", rest, 3) + line += f" #{seqnum:03d} > {pretty_macbin(to_mac)}" + + if msg_type in (MSG_DATA, MSG_DATA_ACK): + data = rest[3 + struct.calcsize("!B6s"):] + + if len(data) > 4: + line += " [\n" + INDENT + else: + line += " [ " + + line += hexdump(data) + " ]" + + else: + # Raw message, not P2P + if len(rest) > 4: + line += f"[\n{INDENT}{hexdump(data)} ]" + else: + line += f" [ {hexdump(data)} ]" + + print(textwrap.fill(line, 80, subsequent_indent=INDENT, )) + +def main(): + server_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + server_socket.bind(('', 32888)) + + while True: + message, address = server_socket.recvfrom(1024) + handle_msg(address, message) + +if __name__ == "__main__": + main() diff --git a/tools/rayMapEditor/.vscode/launch.json b/tools/rayMapEditor/.vscode/launch.json new file mode 100644 index 000000000..783125e49 --- /dev/null +++ b/tools/rayMapEditor/.vscode/launch.json @@ -0,0 +1,16 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "ray_map_editor", + "type": "python", + "request": "launch", + "program": "ray_map_editor.py", + "console": "integratedTerminal", + "justMyCode": true + } + ] +} \ No newline at end of file diff --git a/tools/rayMapEditor/.vscode/settings.json b/tools/rayMapEditor/.vscode/settings.json new file mode 100644 index 000000000..aec77a188 --- /dev/null +++ b/tools/rayMapEditor/.vscode/settings.json @@ -0,0 +1,21 @@ +{ + "cSpell.words": [ + "activebackground", + "activeforeground", + "autoseparators", + "borderwidth", + "columnspan", + "DESPAWN", + "highlightbackground", + "highlightcolor", + "highlightthickness", + "insertbackground", + "LANCZOS", + "lineend", + "linestart", + "maxundo", + "padx", + "pady", + "rowspan" + ] +} \ No newline at end of file diff --git a/tools/rayMapEditor/README.md b/tools/rayMapEditor/README.md new file mode 100644 index 000000000..3899e94a8 --- /dev/null +++ b/tools/rayMapEditor/README.md @@ -0,0 +1,142 @@ +# Ray Map Editor + +## Controls + +Right click on tiles and objects in the palette on the left edge to select them. +Right click on the map to place the selected tile or object. + +Left click on the map to see that cell's coordinate and optionally that object's ID in the right text box + +Middle click on the map to drag the whole map around. + +Load, save, or 'save as' the current map and scripts with the buttons on the top. + +## Tiles + +A map is a grid of tiles. The map size is configurable, but no larger than 255x255 tiles. +Each tile has a coordinate, where `{0.0}` is in the top left corner. The coordinate may be checked by right clicking on a tile. + +Each tile on the map must have a background. Backgrounds are in the left column of the palette. +Background properties are intrinsic so wall types cannot be walked through, door types will open and close, etc. +You don't need to program properties of backgrounds. + +Each tile on the may may also have an object. Objects are in the right column of the palette. +Object properties arse also intrinsic so item types will be picked up when touched, enemy types will move around and fight, etc. +Each tile starts with up to one object, though more can be spawned later. +Each object has an ID which *must* be unique, including the IDs of spawned objects. +IDs are automatically assigned when placing objects on the map, but must be manually assigned when using them in scripts. +An object's ID may be checked by right clicking on it. + +## Scripts + +Scripts define interaction in the map. Each script is comprised of an `IF` and `THEN` part. +`IF` defines what must happen for the script to trigger, i.e. kill some enemies or get an item. +`THEN` defines what happens when the script triggers, i.e. open a door or warp to a location. +Any `IF` may be paired with any other `THEN`. + +### IF Operations + +These are the conditions which can trigger scripts. + +| Operation | Value | Arguments | Description | +|----------------|-------|------------------|-------------------------------------------------------------------------------------------------------------------------------------| +| SHOOT_OBJS | 0 | \[IDs\], ORDER | Triggered when all objects are shot (not killed), may either be in the given order or any order | +| SHOOT_WALLS | 1 | \[CELLs\], ORDER | Triggered when all walls are shot, may either be in the given order or any order | +| KILL | 2 | \[IDs\], ORDER | Triggered when all enemies are killed, may be in the given order or any order | +| ENTER | 3 | CELL, \[IDs\] | Triggered when the player enters the given cell. If IDs are not empty, the player must have the given items when entering the cell. | +| GET | 4 | \[IDs\] | Triggered when all items with given IDs are obtained | +| TOUCH | 5 | ID | Triggered when item with the given ID is touched (i.e. warp gate) | +| BUTTON_PRESSED | 6 | BTN | Triggered when the button is pressed (useful for tutorial) | +| TIME_ELAPSED | 7 | TIME | Triggered after the given time elapses from the start of the level | + +### THEN Operations + +These are the actions that occur when a script is triggered + +| Operation | Value | Arguments | Description | +|-----------|-------|------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| OPEN | 8 | \[CELLs\] | Open all doors on the given cells | +| CLOSE | 9 | \[CELLs\] | Close all doors on the given cells | +| SPAWN | 10 | \[SPAWNs\] | Spawn objects of the given types with the given IDs in the given cells. May be an item or enemy. If an object with that ID exists already, it will not be spawned. | +| DESPAWN | 11 | \[IDs\] | Immediately remove the given IDs from the map. May be item or enemy. | +| DIALOG | 12 | TEXT | Display the text in a dialog window | +| WARP | 13 | CELL | Warp the player to the given cell | +| WIN | 14 | | Finish the level | + +### Script Element Syntax + +These are the elements that are used for `IF` and `THEN` arguments. +Arguments, arrays, CELLs, and SPAWNs all have different delimiters to make parsing easier. + +| Element | Syntax | Notes | +|-----------|--------------------|--------------------------------------------------------------------------------------------------------------------------------------------| +| Arguments | `(a; b; c)` | Arguments are only used for Operations | +| Arrays | `[a, b, c]` | May be arrays of CELLs, SPAWNs, or IDs | +| CELL | `{x. y}` | Only has x and y components | +| SPAWN | `{TYPE- ID- x. y}` | TYPE is any `tileType` (see below). ID, x, and y are integers | +| ORDER | `abc` | `IN_ORDER` or `ANY_ORDER` | +| TEXT | `abc` | Not quoted, cannot use the characters `(` or `)` | +| ID | `0` | Integer from 0 to 255 | +| BTN | `0` | Integer from 0 to 65535, see [`buttonBit_t`](https://github.com/AEFeinstein/Swadge-IDF-5.0/blob/main/components/hdw-btn/include/hdw-btn.h) | +| TIME | `0` | Integer from 0 to 2147483647, in milliseconds | + +### Tile Types + +These are the objects that can be spawned + +| Object | Notes | +|---------------------------|----------------------------------------------------| +| `BG_FLOOR` | Normal floor | +| `BG_FLOOR_WATER` | Water floor, moves slowly | +| `BG_FLOOR_LAVA` | Lava floor, takes damage | +| `BG_WALL_1` | Wall variant 1 | +| `BG_WALL_2` | Wall variant 2 | +| `BG_WALL_3` | Wall variant 3 | +| `BG_DOOR` | Door, normal beam | +| `BG_DOOR_CHARGE` | Door, charge beam | +| `BG_DOOR_MISSILE` | Door, missile | +| `BG_DOOR_ICE` | Door, ice beam | +| `BG_DOOR_XRAY` | Door, x-ray beam | +| `OBJ_ENEMY_START_POINT` | Starting point, not a real object | +| `OBJ_ENEMY_NORMAL` | Enemy type, weak to normal beam | +| `OBJ_ENEMY_STRONG` | Enemy type, weak to charge beam | +| `OBJ_ENEMY_ARMORED` | Enemy type, weak to missile | +| `OBJ_ENEMY_FLAMING` | Enemy type, weak to ice beam | +| `OBJ_ENEMY_HIDDEN` | Enemy type, weak to x-ray beam | +| `OBJ_ITEM_BEAM` | Power-up, normal beam | +| `OBJ_ITEM_CHARGE_BEAM` | Power-up, charge beam | +| `OBJ_ITEM_MISSILE` | Power-up, missiles (also missile capacity upgrade) | +| `OBJ_ITEM_ICE` | Power-up, ice beam | +| `OBJ_ITEM_XRAY` | Power-up, x-ray visor | +| `OBJ_ITEM_SUIT_WATER` | Power-up, suit, water resistance | +| `OBJ_ITEM_SUIT_LAVA` | Power-up, suit, lava resistance | +| `OBJ_ITEM_ENERGY_TANK` | Power-up, energy tank | +| `OBJ_ITEM_KEY` | Access item, key | +| `OBJ_ITEM_ARTIFACT` | Access item, artifact | +| `OBJ_ITEM_PICKUP_ENERGY` | Pickup, energy | +| `OBJ_ITEM_PICKUP_MISSILE` | Pickup, missiles | +| `OBJ_SCENERY_TERMINAL` | Scenery, computer terminal | + +### Script Examples + +``` +IF SHOOT_OBJS([0, 1, 2, 3]; IN_ORDER) THEN OPEN([{0.1}, {2.3}]) +IF SHOOT_WALLS([{4.5}, {6.7}]; ANY_ORDER) THEN CLOSE([{0.1}, {2.3}]) +IF KILL([0, 1, 2, 3]; IN_ORDER) THEN SPAWN([{ENEMY_DRAGON-98-0.1},{ENEMY_SKELETON-99-2.3}]) +IF ENTER({4.5}; [6, 7, 8]) THEN DESPAWN([98, 99]) +IF GET([0, 1, 2, 3]) THEN WIN() +IF TOUCH(7) THEN WARP({5.3}) +IF BUTTON_PRESSED(512) THEN DIALOG(BUTTON PRESSED) +IF TIME_ELAPSED(10000) THEN CLOSE([{7.7}, {8.8}]) +``` + +## RMD File Format + +1. Map Width (8 bit) +1. Map Height (8 bit) +1. The Map (list of cells in row order) + 1. Background for the cell (8 bit) + 1. Object for the cell (8 bit) + 1. Object ID for the cell, if there is an object (8 bit) +1. Number of Scripts (8 bit) +1. Scripts (Variable length, see `rme_script.toBytes()`) diff --git a/tools/rayMapEditor/imgs/DELETE.png b/tools/rayMapEditor/imgs/DELETE.png new file mode 100644 index 000000000..bad6c4597 Binary files /dev/null and b/tools/rayMapEditor/imgs/DELETE.png differ diff --git a/tools/rayMapEditor/imgs/OBJ_ENEMY_START_POINT.png b/tools/rayMapEditor/imgs/OBJ_ENEMY_START_POINT.png new file mode 100644 index 000000000..eba54ae4d Binary files /dev/null and b/tools/rayMapEditor/imgs/OBJ_ENEMY_START_POINT.png differ diff --git a/tools/rayMapEditor/ray_map_editor.py b/tools/rayMapEditor/ray_map_editor.py new file mode 100644 index 000000000..652f4c0c6 --- /dev/null +++ b/tools/rayMapEditor/ray_map_editor.py @@ -0,0 +1,24 @@ +#!/usr/bin/python + +from rme_view import view +from rme_model import model +from rme_controller import controller + + +def main(): + v: view = view() + m: model = model(32, 16) + c: controller = controller() + + c.setModel(m) + v.setController(c) + v.setModel(m) + m.setView(v) + + v.redraw() + + v.mainloop() + + +if __name__ == '__main__': + main() diff --git a/tools/rayMapEditor/requirements.txt b/tools/rayMapEditor/requirements.txt new file mode 100644 index 000000000..c54bdf104 Binary files /dev/null and b/tools/rayMapEditor/requirements.txt differ diff --git a/tools/rayMapEditor/rme_controller.py b/tools/rayMapEditor/rme_controller.py new file mode 100644 index 000000000..40f8db776 --- /dev/null +++ b/tools/rayMapEditor/rme_controller.py @@ -0,0 +1,40 @@ +from rme_tiles import * + + +class controller: + + def __init__(self): + self.isMapLeftClicked: bool = False + self.isPaletteClicked: bool = False + + def setModel(self, m): + from rme_model import model + self.m: model = m + + def clickPalette(self, x, y): + self.isPaletteClicked = True + type = self.m.getPaletteType(x, y) + if type is not None: + self.m.setSelectedTileType(type, x, y) + + def leftClickMap(self, x, y): + self.isMapLeftClicked = True + if self.m.getSelectedTileType() in bgTiles: + self.m.setMapTileBg(x, y, self.m.selectedTileType) + elif self.m.getSelectedTileType() in objTiles: + self.m.setMapTileObj(x, y, self.m.selectedTileType) + + def releaseClick(self): + self.isMapLeftClicked = False + self.isPaletteClicked = False + + def moveMouseMap(self, cellX, cellY): + if self.isMapLeftClicked: + self.leftClickMap(cellX, cellY) + + def moveMousePalette(self, cellX, cellY): + if self.isPaletteClicked: + self.clickPalette(cellX, cellY) + + def rightClickMap(self, x, y): + self.m.setSelectedCell(x, y) diff --git a/tools/rayMapEditor/rme_model.py b/tools/rayMapEditor/rme_model.py new file mode 100644 index 000000000..1e783f62d --- /dev/null +++ b/tools/rayMapEditor/rme_model.py @@ -0,0 +1,207 @@ +from rme_tiles import * +from rme_script_editor import * +from rme_view import NUM_PALETTE_ROWS +from io import TextIOWrapper + + +class model: + + def __init__(self, width: int, height: int): + self.tileMap: list[list[tile]] = [ + [tile() for y in range(height)] for x in range(width)] + self.selectedTileType: tileType = None + self.scripts: list[rme_script] = [] + self.splitter: rme_scriptSplitter = rme_scriptSplitter() + self.currentId: int = 0 + self.usedIds: list[int] = [] + + def setView(self, v): + from rme_view import view + self.v: view = v + + def setMapSize(self, newWidth: int, newHeight: int): + print(str(newWidth) + ' x ' + str(newHeight)) + + # Make a new map + newTileMap: list[list[tile]] = [ + [tile() for y in range(newHeight)] for x in range(newWidth)] + + # Copy as much as one can from old to new + minWidth: int = min(newWidth, self.getMapWidth()) + minHeight: int = min(newHeight, self.getMapHeight()) + for y in range(minHeight): + for x in range(minWidth): + newTileMap[x][y] = self.tileMap[x][y] + + # Set the new map + self.tileMap = newTileMap + + def getMapWidth(self): + return len(self.tileMap) + + def getMapHeight(self): + return len(self.tileMap[0]) + + def setMapTileBg(self, x: int, y: int, bg: tileType): + if (0 <= x and x < len(self.tileMap)): + if (0 <= y and y < len(self.tileMap[x])): + if not self.tileMap[x][y].background == bg: + self.tileMap[x][y].setBg(bg) + self.v.drawMapCell(x, y) + + def setMapTileObj(self, x: int, y: int, obj: tileType): + if (0 <= x and x < len(self.tileMap)): + if (0 <= y and y < len(self.tileMap[x])): + if tileType.DELETE == obj: + if tileType.EMPTY != self.tileMap[x][y].object: + self.usedIds.remove(self.tileMap[x][y].objectId) + self.tileMap[x][y].setObj(tileType.EMPTY, -1) + self.v.drawMapCell(x, y) + elif not self.tileMap[x][y].object == obj: + objId = self.getNextId() + if -1 != objId: + self.tileMap[x][y].setObj(obj, objId) + self.v.drawMapCell(x, y) + + def getNextId(self): + if len(self.usedIds) == 256: + return -1 + while True: + if self.currentId not in self.usedIds: + self.usedIds.append(self.currentId) + return self.currentId + else: + self.currentId = (self.currentId + 1) % 256 + + def getPaletteType(self, x, y): + if 0 == x: + # backgrounds in the first column + if 0 <= y and y < len(bgTiles): + return bgTiles[y] + else: + # objects i the other columns + y = y + NUM_PALETTE_ROWS * (x - 1) + if 0 <= y and y < len(objTiles): + return objTiles[y] + return None + + def setSelectedTileType(self, type, x, y): + self.selectedTileType = type + self.v.drawSelectedTile(self.selectedTileType, x, y) + + def getSelectedTileType(self): + return self.selectedTileType + + def setSelectedCell(self, x, y): + if (0 <= x and x < len(self.tileMap)): + if (0 <= y and y < len(self.tileMap[x])): + self.v.selectCell(x, y, self.tileMap[x][y].objectId) + + def setScripts(self, scripts: list[str]) -> None: + self.scripts: list[rme_script] = [] + for script in scripts: + self.scripts.append(rme_script( + string=script, splitter=self.splitter)) + + def save(self, outFile: TextIOWrapper) -> bool: + # Construct file bytes + fileBytes: bytearray = bytearray() + + # Write map dimensions + if self.getMapHeight() > 255 or self.getMapWidth() > 255: + # TODO display error + print("MAP TOO BIG!!") + return False + fileBytes.append(self.getMapWidth()) + fileBytes.append(self.getMapHeight()) + + # Write map tiles + for y in range(self.getMapHeight()): + for x in range(self.getMapWidth()): + fileBytes.append(self.tileMap[x][y].background.value) + fileBytes.append(self.tileMap[x][y].object.value) + # Only append IDs if there is an object + if tileType.EMPTY is not self.tileMap[x][y].object: + fileBytes.append(self.tileMap[x][y].objectId) + + # Write number of scripts + numScripts: int = sum(x is not None and x.isValid() + for x in self.scripts) + if numScripts > 255: + # TODO display error + print("TOO MANY SCRIPTS!!") + return False + fileBytes.append(numScripts) + + # Write script data + for script in self.scripts: + if script is not None and script.isValid(): + sb = script.toBytes() + if (len(sb) > 255): + # TODO display error + print("SCRIPT TOO BIG!!") + return False + fileBytes.append(len(sb)) + fileBytes.extend(sb) + else: + # TODO display warning + print("INVALID SCRIPT!!") + + # Write bytes to file + return len(fileBytes) == outFile.write(fileBytes) + + def load(self, inFile: TextIOWrapper) -> bool: + data: bytes = inFile.read() + idx: int = 0 + + # Read width and height + mapWidth: int = data[idx] + idx = idx + 1 + mapHeight: int = data[idx] + idx = idx + 1 + + # Make empty tiles and used IDs + self.tileMap = [ + [tile() for y in range(mapHeight)] for x in range(mapWidth)] + self.usedIds = [] + self.currentId = 0 + + # Nothing selected yet + self.selectedTileType = None + + # Read map tiles + for y in range(self.getMapHeight()): + for x in range(self.getMapWidth()): + # Read background + self.tileMap[x][y].background = tileType._value2member_map_[ + data[idx]] + idx = idx + 1 + # Read Object + self.tileMap[x][y].object = tileType._value2member_map_[ + data[idx]] + idx = idx + 1 + # Read optional object ID + if tileType.EMPTY is not self.tileMap[x][y].object: + self.tileMap[x][y].objectId = data[idx] + idx = idx + 1 + # Note this ID is used + self.usedIds.append(self.tileMap[x][y].objectId) + + # Read number of scripts + numScripts: int = data[idx] + idx = idx + 1 + + # Empty the list of scripts + self.scripts = [] + + # Read each script + for si in range(numScripts): + # Read script length + sLen: int = data[idx] + idx = idx + 1 + # Read script + self.scripts.append(rme_script(bytes=data[idx: idx + sLen])) + idx = idx + sLen + + # Everything loaded + return True diff --git a/tools/rayMapEditor/rme_script_editor.py b/tools/rayMapEditor/rme_script_editor.py new file mode 100644 index 000000000..a7d3df829 --- /dev/null +++ b/tools/rayMapEditor/rme_script_editor.py @@ -0,0 +1,578 @@ +import re +from enum import Enum +from rme_tiles import tileType + +# Argument keys +kCell: str = 'cell' +kCells: str = 'cells' +kId: str = 'id' +kIds: str = 'ids' +kSpawns: str = 'spawns' +kOrder: str = 'order' +kBtn: str = 'btn' +kTms: str = 'tMs' +kText: str = 'text' + + +class ifOpType(Enum): + SHOOT_OBJS = 0 + SHOOT_WALLS = 1 + KILL = 2 + ENTER = 3 + GET = 4 + TOUCH = 5 + BUTTON_PRESSED = 6 + TIME_ELAPSED = 7 + + +class thenOpType(Enum): + OPEN = 8 + CLOSE = 9 + SPAWN = 10 + DESPAWN = 11 + DIALOG = 12 + WARP = 13 + WIN = 14 + + +class orderType(Enum): + IN_ORDER = 0 + ANY_ORDER = 1 + + +class spawn: + def __init__(self, type: tileType, id: int, x: int, y: int) -> None: + self.type = type + self.id = id + self.x = x + self.y = y + + +class rme_scriptSplitter: + + def __init__(self) -> None: + argsRegex: str = '(\([A-Z0-9_,;\.\-\[\]\{\}\s]*\))' + + regex = 'IF\s+(' + + first = False + for ifOp in ifOpType.__members__.items(): + if not first: + first = True + else: + regex = regex + '|' + regex = regex + ifOp[0] + + regex = regex + ')\s*' + regex = regex + argsRegex + regex = regex + '\s+THEN\s+(' + + first = False + for thenOp in thenOpType.__members__.items(): + if not first: + first = True + else: + regex = regex + '|' + regex = regex + thenOp[0] + regex = regex + ')\s*' + regex = regex + argsRegex + + self.pattern: re.Pattern = re.compile(regex, flags=re.IGNORECASE) + pass + + def splitScript(self, scriptLine: str) -> list[str]: + match = self.pattern.fullmatch(scriptLine) + if None != match: + return [match.group(1), match.group(2), match.group(3), match.group(4)] + return None + + +class rme_script: + def __init__(self, bytes: bytearray = None, string: str = None, splitter: rme_scriptSplitter = None) -> None: + # Add members + self.resetScript() + # Parse depending on what args we get + if bytes is not None: + self.fromBytes(bytes) + elif string is not None and splitter is not None: + parts = splitter.splitScript(string) + if parts is not None and len(parts) == 4: + self.fromString(parts[0], parts[1], parts[2], parts[3]) + else: + # Failure, reset the script + self.resetScript() + pass + + def __parseArgs(self, args: str) -> list[str]: + # Args are in the form (a;b;c) + result = re.match(r'\s*\((.*)\)\s*', args) + if result: + return [s.strip() for s in re.split(r';', result.group(1).strip())] + return None + + def __parseArray(self, array: str) -> list[str]: + # Arrays are in the form [a,b,c] + result = re.match(r'\s*\[(.*)\]\s*', array) + if result: + return [s.strip() for s in re.split(r',', result.group(1).strip())] + return None + + def __parseInt(self, integer: str) -> int: + # Integers are integers + return int(integer.strip()) + + def __parseOrder(self, order: str) -> orderType: + # Order is either IN_ORDER or ANY_ORDER + for type in orderType.__members__.items(): + if order == type[0]: + return type[1] + return None + + def __parseCell(self, cell: str) -> list[int]: + # Cells are in the form {a.b} + result = re.match(r'{\s*(\d+)\s*\.\s*(\d+)\s*}', cell.strip()) + if result: + return [int(result.group(1)), int(result.group(2))] + return None + + def __parseSpawn(self, spawnStr: str) -> spawn: + result = re.match( + r'{\s*([a-zA-Z_]+)\s*-\s*(\d+)\s*-\s*(\d+)\s*\.\s*(\d+)\s*}', spawnStr.strip()) + if result: + type: tileType = None + for a in tileType: + if a.name == 'OBJ_' + result.group(1): + type = a + break + if type is not None: + return spawn(type, int(result.group(2)), int(result.group(3)), int(result.group(4))) + return None + + def __parseText(self, text: str) -> str: + # Text is a string, not quoted + return text.strip() + + def __isListNotValid(self, array: list) -> bool: + return (array is None) or (0 == len(array)) or (None in array) + + def toString(self) -> str: + + # Stringify the if args + ifArgArray = [] + if kCell in self.ifArgs.keys(): + ifArgArray.append( + '{' + str(self.ifArgs[kCell][0]) + '.' + str(self.ifArgs[kCell][1]) + '}') + if kIds in self.ifArgs.keys(): + ifArgArray.append('[' + ', '.join(str(e) + for e in self.ifArgs[kIds]) + ']') + if kCells in self.ifArgs.keys(): + ifArgArray.append( + '[' + ', '.join('{' + str(e[0]) + '.' + str(e[1]) + '}' for e in self.ifArgs[kCells]) + ']') + if kOrder in self.ifArgs.keys(): + ifArgArray.append(self.ifArgs[kOrder].name) + if kId in self.ifArgs.keys(): + ifArgArray.append(str(self.ifArgs[kId])) + if kBtn in self.ifArgs.keys(): + ifArgArray.append(str(self.ifArgs[kBtn])) + if kTms in self.ifArgs.keys(): + ifArgArray.append(str(self.ifArgs[kTms])) + + # Stringify the then args + thenArgArray = [] + if kCells in self.thenArgs.keys(): + thenArgArray.append( + '[' + ', '.join('{' + str(e[0]) + '.' + str(e[1]) + '}' for e in self.thenArgs[kCells]) + ']') + if kCell in self.thenArgs.keys(): + thenArgArray.append( + '{' + str(self.thenArgs[kCell][0]) + '.' + str(self.thenArgs[kCell][1]) + '}') + if kIds in self.thenArgs.keys(): + thenArgArray.append('[' + ', '.join(str(e) + for e in self.thenArgs[kIds]) + ']') + if kText in self.thenArgs.keys(): + thenArgArray.append(self.thenArgs[kText]) + if kSpawns in self.thenArgs.keys(): + thenArgArray.append( + '[' + ', '.join('{' + e.type.name.removeprefix('OBJ_') + '-' + str(e.id) + '-' + str(e.x) + '.' + str(e.y) + '}' for e in self.thenArgs[kSpawns]) + ']') + + # Stitch it all together + return 'IF ' + self.ifOp.name + '(' + '; '.join(ifArgArray) + ') THEN ' + self.thenOp.name + '(' + '; '.join(thenArgArray) + ')' + + def fromString(self, if_op: str, if_args: str, then_op: str, then_args: str) -> bool: + try: + # Split the args + argParts = self.__parseArgs(if_args) + + # Parse the IF part + if ifOpType.SHOOT_OBJS.name == if_op: + # Set the operation type + self.ifOp = ifOpType.SHOOT_OBJS + # Parse the args + self.ifArgs[kIds] = [self.__parseInt( + s) for s in self.__parseArray(argParts[0])] + self.ifArgs[kOrder] = self.__parseOrder(argParts[1]) + # Validate the args + if self.__isListNotValid(self.ifArgs[kIds]) or self.ifArgs[kOrder] is None: + self.resetScript() + return False + + elif ifOpType.SHOOT_WALLS.name == if_op: + self.ifOp = ifOpType.SHOOT_WALLS + # Parse the args + self.ifArgs[kCells] = [self.__parseCell( + s) for s in self.__parseArray(argParts[0])] + self.ifArgs[kOrder] = self.__parseOrder(argParts[1]) + # Validate the args + if self.__isListNotValid(self.ifArgs[kCells]) or self.ifArgs[kOrder] is None: + self.resetScript() + return False + + elif ifOpType.KILL.name == if_op: + # Set the operation type + self.ifOp = ifOpType.KILL + # Parse the args + self.ifArgs[kIds] = [self.__parseInt( + s) for s in self.__parseArray(argParts[0])] + self.ifArgs[kOrder] = self.__parseOrder(argParts[1]) + # Validate the args + if self.__isListNotValid(self.ifArgs[kIds]) or self.ifArgs[kOrder] is None: + self.resetScript() + return False + + elif ifOpType.ENTER.name == if_op: + self.ifOp = ifOpType.ENTER + # Parse the args + self.ifArgs[kCell] = self.__parseCell(argParts[0]) + self.ifArgs[kIds] = [self.__parseInt( + s) for s in self.__parseArray(argParts[1])] + # Validate the args + if self.__isListNotValid(self.ifArgs[kIds]) or self.ifArgs[kCell] is None: + self.resetScript() + return False + + elif ifOpType.GET.name == if_op: + self.ifOp = ifOpType.GET + # Parse the args + self.ifArgs[kIds] = [self.__parseInt( + s) for s in self.__parseArray(argParts[0])] + # Validate the args + if self.__isListNotValid(self.ifArgs[kIds]): + self.resetScript() + return False + + elif ifOpType.TOUCH.name == if_op: + self.ifOp = ifOpType.TOUCH + # Parse the args + self.ifArgs[kId] = self.__parseInt(argParts[0]) + # Validate the args + if self.ifArgs[kId] is None: + return False + + elif ifOpType.BUTTON_PRESSED.name == if_op: + self.ifOp = ifOpType.BUTTON_PRESSED + # Parse the args + self.ifArgs[kBtn] = self.__parseInt(argParts[0]) + # Validate the args + if self.ifArgs[kBtn] is None: + return False + + elif ifOpType.TIME_ELAPSED.name == if_op: + self.ifOp = ifOpType.TIME_ELAPSED + # Parse the arg + self.ifArgs[kTms] = self.__parseInt(argParts[0]) + # Validate the args + if self.ifArgs[kTms] is None: + return False + else: + self.resetScript() + return False + + # Split the args + argParts = self.__parseArgs(then_args) + + # Parse the THEN part + if thenOpType.OPEN.name == then_op: + # Set the operation + self.thenOp = thenOpType.OPEN + # Parse the args + self.thenArgs[kCells] = [self.__parseCell( + s) for s in self.__parseArray(argParts[0])] + # Validate the args + if self.__isListNotValid(self.thenArgs[kCells]): + self.resetScript() + return False + + elif thenOpType.CLOSE.name == then_op: + # Set the operation + self.thenOp = thenOpType.CLOSE + # Parse the args + self.thenArgs[kCells] = [self.__parseCell( + s) for s in self.__parseArray(argParts[0])] + # Validate the args + if self.__isListNotValid(self.thenArgs[kCells]): + self.resetScript() + return False + + elif thenOpType.SPAWN.name == then_op: + self.thenOp = thenOpType.SPAWN + # Parse the args + self.thenArgs[kSpawns] = [self.__parseSpawn( + s) for s in self.__parseArray(argParts[0])] + # Validate the args + if self.__isListNotValid(self.thenArgs[kSpawns]): + self.resetScript() + return False + + elif thenOpType.DESPAWN.name == then_op: + self.thenOp = thenOpType.DESPAWN + # Parse the args + self.thenArgs[kIds] = [self.__parseInt( + s) for s in self.__parseArray(argParts[0])] + # Validate the args + if self.__isListNotValid(self.thenArgs[kIds]): + self.resetScript() + return False + + elif thenOpType.DIALOG.name == then_op: + # Set the operation + self.thenOp = thenOpType.DIALOG + # Parse the args + self.thenArgs[kText] = self.__parseText(argParts[0]) + # Validate the args + if self.thenArgs[kText] is None: + return False + + elif thenOpType.WARP.name == then_op: + # Set the operation + self.thenOp = thenOpType.WARP + # Parse the args + self.thenArgs[kCell] = self.__parseCell(argParts[0]) + # Validate the args + if self.thenArgs[kCell] is None: + return False + + elif thenOpType.WIN.name == then_op: + # Set the operation + self.thenOp = thenOpType.WIN + # No arguments + + else: + self.resetScript() + return False + + return True + except Exception as e: + # print(e) + self.resetScript() + return False + + def toBytes(self) -> bytearray: + bytes: bytearray = bytearray() + + # Append the IF operation + bytes.append(self.ifOp.value) + + # Append the IF arguments, order matters + if kCell in self.ifArgs.keys(): + bytes.append(self.ifArgs[kCell][0]) + bytes.append(self.ifArgs[kCell][1]) + if kIds in self.ifArgs.keys(): + bytes.append(len(self.ifArgs[kIds])) + for id in self.ifArgs[kIds]: + bytes.append(id) + if kCells in self.ifArgs.keys(): + bytes.append(len(self.ifArgs[kCells])) + for cell in self.ifArgs[kCells]: + bytes.append(cell[0]) + bytes.append(cell[1]) + if kOrder in self.ifArgs.keys(): + if orderType.IN_ORDER == self.ifArgs[kOrder]: + bytes.append(0) + elif orderType.ANY_ORDER == self.ifArgs[kOrder]: + bytes.append(1) + if kId in self.ifArgs.keys(): + bytes.append(self.ifArgs[kId]) + if kBtn in self.ifArgs.keys(): + bytes.append((self.ifArgs[kBtn] >> 8) & 255) + bytes.append((self.ifArgs[kBtn] >> 0) & 255) + if kTms in self.ifArgs.keys(): + bytes.append((self.ifArgs[kTms] >> 24) & 255) + bytes.append((self.ifArgs[kTms] >> 16) & 255) + bytes.append((self.ifArgs[kTms] >> 8) & 255) + bytes.append((self.ifArgs[kTms] >> 0) & 255) + + # Append the ELSE operation + bytes.append(self.thenOp.value) + + # Append the ELSE arguments, order matters + if kCells in self.thenArgs.keys(): + bytes.append(len(self.thenArgs[kCells])) + for cell in self.thenArgs[kCells]: + bytes.append(cell[0]) + bytes.append(cell[1]) + if kCell in self.thenArgs.keys(): + bytes.append(self.thenArgs[kCell][0]) + bytes.append(self.thenArgs[kCell][1]) + if kIds in self.thenArgs.keys(): + bytes.append(len(self.thenArgs[kIds])) + for id in self.thenArgs[kIds]: + bytes.append(id) + if kText in self.thenArgs.keys(): + bytes.append(len(self.thenArgs[kText])) + bytes.extend(self.thenArgs[kText].encode()) + if kSpawns in self.thenArgs.keys(): + bytes.append(len(self.thenArgs[kSpawns])) + for spawnObj in self.thenArgs[kSpawns]: + bytes.append(spawnObj.type.value) + bytes.append(spawnObj.id) + bytes.append(spawnObj.x) + bytes.append(spawnObj.y) + + return bytes + + def fromBytes(self, bytes: bytearray): + + # Index to read bytes + idx: int = 0 + + # Read the if operation + self.ifOp = ifOpType._value2member_map_[bytes[idx]] + idx = idx + 1 + + # Read the if args + if self.ifOp == ifOpType.SHOOT_OBJS: + # Read IDs + numIds: int = bytes[idx] + idx = idx + 1 + self.ifArgs[kIds] = [] + for id in range(numIds): + self.ifArgs[kIds].append(bytes[idx]) + idx = idx + 1 + # Read order + self.ifArgs[kOrder] = orderType._value2member_map_[bytes[idx]] + idx = idx + 1 + elif self.ifOp == ifOpType.SHOOT_WALLS: + # Read Cells + numCells: int = bytes[idx] + idx = idx + 1 + self.ifArgs[kCells] = [] + for id in range(numCells): + self.ifArgs[kCells].append([bytes[idx], bytes[idx + 1]]) + idx = idx + 2 + # Read order + self.ifArgs[kOrder] = orderType._value2member_map_[bytes[idx]] + idx = idx + 1 + pass + elif self.ifOp == ifOpType.KILL: + # Read the IDs + numIds: int = bytes[idx] + idx = idx + 1 + self.ifArgs[kIds] = [] + for id in range(numIds): + self.ifArgs[kIds].append(bytes[idx]) + idx = idx + 1 + # Read order + self.ifArgs[kOrder] = orderType._value2member_map_[bytes[idx]] + idx = idx + 1 + elif self.ifOp == ifOpType.ENTER: + # Read the cell + self.ifArgs[kCell] = [bytes[idx], bytes[idx + 1]] + idx = idx + 2 + # Read IDs + numIds: int = bytes[idx] + idx = idx + 1 + self.ifArgs[kIds] = [] + for id in range(numIds): + self.ifArgs[kIds].append(bytes[idx]) + idx = idx + 1 + elif self.ifOp == ifOpType.GET: + # Read IDs + numIds: int = bytes[idx] + idx = idx + 1 + self.ifArgs[kIds] = [] + for id in range(numIds): + self.ifArgs[kIds].append(bytes[idx]) + idx = idx + 1 + elif self.ifOp == ifOpType.TOUCH: + self.ifArgs[kId] = bytes[idx] + idx = idx + 1 + elif self.ifOp == ifOpType.BUTTON_PRESSED: + self.ifArgs[kBtn] = \ + (bytes[idx + 0] << 8) + \ + (bytes[idx + 1]) + idx = idx + 2 + elif self.ifOp == ifOpType.TIME_ELAPSED: + self.ifArgs[kTms] = \ + (bytes[idx + 0] << 24) + \ + (bytes[idx + 1] << 16) + \ + (bytes[idx + 2] << 8) + \ + (bytes[idx + 3]) + idx = idx + 4 + else: + self.resetScript() + return + + # Read the then operation + self.thenOp = thenOpType._value2member_map_[bytes[idx]] + idx = idx + 1 + + # Read the then args + if self.thenOp == thenOpType.OPEN: + # Read Cells + numCells: int = bytes[idx] + idx = idx + 1 + self.thenArgs[kCells] = [] + for id in range(numCells): + self.thenArgs[kCells].append([bytes[idx], bytes[idx + 1]]) + idx = idx + 2 + elif self.thenOp == thenOpType.CLOSE: + # Read Cells + numCells: int = bytes[idx] + idx = idx + 1 + self.thenArgs[kCells] = [] + for id in range(numCells): + self.thenArgs[kCells].append([bytes[idx], bytes[idx + 1]]) + idx = idx + 2 + elif self.thenOp == thenOpType.SPAWN: + # Read spawns + numSpawns: int = bytes[idx] + idx = idx + 1 + self.thenArgs[kSpawns] = [] + for sp in range(numSpawns): + self.thenArgs[kSpawns].append(spawn(tileType._value2member_map_[ + bytes[idx]], bytes[idx + 1], bytes[idx + 2], bytes[idx + 3])) + idx = idx + 4 + pass + elif self.thenOp == thenOpType.DESPAWN: + # Read IDs + numIds: int = bytes[idx] + idx = idx + 1 + self.thenArgs[kIds] = [] + for id in range(numIds): + self.thenArgs[kIds].append(bytes[idx]) + idx = idx + 1 + elif self.thenOp == thenOpType.DIALOG: + # Read Text + textLen: int = bytes[idx] + idx = idx + 1 + self.thenArgs[kText] = str(bytes[idx:idx + textLen], 'ascii') + elif self.thenOp == thenOpType.WARP: + # Read the cell + self.thenArgs[kCell] = [bytes[idx], bytes[idx + 1]] + idx = idx + 2 + elif self.thenOp == thenOpType.WIN: + # No args + pass + else: + self.resetScript() + return + + return + + def isValid(self) -> bool: + return self.ifOp is not None and self.thenOp is not None + + def resetScript(self) -> None: + self.ifOp: ifOpType = None + self.ifArgs = {} + self.thenOp: thenOpType = None + self.thenArgs = {} diff --git a/tools/rayMapEditor/rme_tiles.py b/tools/rayMapEditor/rme_tiles.py new file mode 100644 index 000000000..65e569028 --- /dev/null +++ b/tools/rayMapEditor/rme_tiles.py @@ -0,0 +1,121 @@ + +from enum import Enum + +# Bits used for tile type construction, topmost bit +BG = 0x00 +OBJ = 0x80 +# Types of background, next two top bits +META = 0x00 +FLOOR = 0x20 +WALL = 0x40 +DOOR = 0x60 +# Types of objects, next two top bits +ITEM = 0x00 +ENEMY = 0x20 +BULLET = 0x40 +SCENERY = 0x60 +# Bottom five bits are used for uniqueness + +class tileType(Enum): + # Special empty type + EMPTY = (BG | META | 0) + # Special delete tile + DELETE = (BG | META | 1) + # Background tiles + BG_FLOOR = (BG | FLOOR | 1) + BG_FLOOR_WATER = (BG | FLOOR | 2) + BG_FLOOR_LAVA = (BG | FLOOR | 3) + BG_CEILING = (BG | FLOOR | 4) + BG_WALL_1 = (BG | WALL | 1) + BG_WALL_2 = (BG | WALL | 2) + BG_WALL_3 = (BG | WALL | 3) + BG_DOOR = (BG | DOOR | 1) + BG_DOOR_CHARGE = (BG | DOOR | 2) + BG_DOOR_MISSILE = (BG | DOOR | 3) + BG_DOOR_ICE = (BG | DOOR | 4) + BG_DOOR_XRAY = (BG | DOOR | 5) + BG_DOOR_SCRIPT = (BG | DOOR | 6) + # Self and Enemies + OBJ_ENEMY_START_POINT = (OBJ | ENEMY | 1) + OBJ_ENEMY_NORMAL = (OBJ | ENEMY | 2) + OBJ_ENEMY_STRONG = (OBJ | ENEMY | 3) + OBJ_ENEMY_ARMORED = (OBJ | ENEMY | 4) + OBJ_ENEMY_FLAMING = (OBJ | ENEMY | 5) + OBJ_ENEMY_HIDDEN = (OBJ | ENEMY | 6) + OBJ_ENEMY_BOSS = (OBJ | ENEMY | 7) + # Power-ups + OBJ_ITEM_BEAM = (OBJ | ITEM | 1) + OBJ_ITEM_CHARGE_BEAM = (OBJ | ITEM | 2) + OBJ_ITEM_MISSILE = (OBJ | ITEM | 3) + OBJ_ITEM_ICE = (OBJ | ITEM | 4) + OBJ_ITEM_XRAY = (OBJ | ITEM | 5) + OBJ_ITEM_SUIT_WATER = (OBJ | ITEM | 6) + OBJ_ITEM_SUIT_LAVA = (OBJ | ITEM | 7) + OBJ_ITEM_ENERGY_TANK = (OBJ | ITEM | 8) + # Permanent non-power-items + OBJ_ITEM_KEY = (OBJ | ITEM | 9) + OBJ_ITEM_ARTIFACT = (OBJ | ITEM | 10) + # Transient items + OBJ_ITEM_PICKUP_ENERGY = (OBJ | ITEM | 11) + OBJ_ITEM_PICKUP_MISSILE = (OBJ | ITEM | 12) + # Bullets + OBJ_BULLET_NORMAL = (OBJ | BULLET | 13) + OBJ_BULLET_CHARGE = (OBJ | BULLET | 14) + OBJ_BULLET_ICE = (OBJ | BULLET | 15) + OBJ_BULLET_MISSILE = (OBJ | BULLET | 16) + OBJ_BULLET_XRAY = (OBJ | BULLET | 17) + # Scenery + OBJ_SCENERY_TERMINAL = (OBJ | SCENERY | 1) + +bgTiles: list[tileType] = [ + tileType.BG_FLOOR, + tileType.BG_FLOOR_WATER, + tileType.BG_FLOOR_LAVA, + tileType.BG_WALL_1, + tileType.BG_WALL_2, + tileType.BG_WALL_3, + tileType.BG_DOOR, + tileType.BG_DOOR_CHARGE, + tileType.BG_DOOR_MISSILE, + tileType.BG_DOOR_ICE, + tileType.BG_DOOR_XRAY, + tileType.BG_DOOR_SCRIPT +] + +objTiles: list[tileType] = [ + tileType.OBJ_ENEMY_START_POINT, + tileType.OBJ_ENEMY_NORMAL, + tileType.OBJ_ENEMY_STRONG, + tileType.OBJ_ENEMY_ARMORED, + tileType.OBJ_ENEMY_FLAMING, + tileType.OBJ_ENEMY_HIDDEN, + tileType.OBJ_ENEMY_BOSS, + tileType.OBJ_ITEM_BEAM, + tileType.OBJ_ITEM_CHARGE_BEAM, + tileType.OBJ_ITEM_MISSILE, + tileType.OBJ_ITEM_ICE, + tileType.OBJ_ITEM_XRAY, + tileType.OBJ_ITEM_SUIT_WATER, + tileType.OBJ_ITEM_SUIT_LAVA, + tileType.OBJ_ITEM_ENERGY_TANK, + tileType.OBJ_ITEM_KEY, + tileType.OBJ_ITEM_ARTIFACT, + tileType.OBJ_ITEM_PICKUP_ENERGY, + tileType.OBJ_ITEM_PICKUP_MISSILE, + tileType.OBJ_SCENERY_TERMINAL, + tileType.DELETE +] + + +class tile: + def __init__(self): + self.background: tileType = tileType.BG_FLOOR + self.object: tileType = tileType.EMPTY + self.objectId: int = -1 + + def setBg(self, bg: tileType): + self.background = bg + + def setObj(self, obj: tileType, id: int): + self.object = obj + self.objectId = id diff --git a/tools/rayMapEditor/rme_view.py b/tools/rayMapEditor/rme_view.py new file mode 100644 index 000000000..6a4a17cd9 --- /dev/null +++ b/tools/rayMapEditor/rme_view.py @@ -0,0 +1,475 @@ +import tkinter as tk +from tkinter import simpledialog +from tkinter.filedialog import askopenfile +from tkinter.filedialog import asksaveasfile +from PIL import Image, ImageTk +from io import TextIOWrapper +import os + +from collections.abc import Mapping + +from rme_tiles import * +from rme_script_editor import rme_scriptSplitter +from rme_script_editor import rme_script + +NUM_PALETTE_ROWS = 11 +class view: + + def __init__(self): + + self.splitter: rme_scriptSplitter = rme_scriptSplitter() + self.currentFilePath: str = None + + self.paletteCellSize: int = 64 + self.mapCellSize: int = 32 + + self.isMapMiddleClicked: bool = False + self.isMapRightClicked: bool = False + + self.selRectX: int = 0 + self.selRectY: int = 0 + + self.highlightRect: int = -1 + self.paletteHighlightRect: int = -1 + + frameBgColor: str = '#181818' + elemBgColor: str = '#1F1F1F' + borderColor: str = '#2A2A2A' + borderHighlightColor: str = '#2B79D7' + fontColor: str = '#CCCCCC' + fontStyle = ('Courier New', 14) + borderThickness: int = 2 + padding: int = 4 + + buttonColor: str = '#2B79D7' + buttonPressedColor: str = '#5191DE' + buttonFontColor: str = '#FFFFFF' + buttonWidth: int = 12 + + # Setup the root and main frames + self.root: tk.Tk = tk.Tk() + self.root.title("Ray Map Editor") + content = tk.Frame(self.root, background=frameBgColor) + frame = tk.Frame(content, background=frameBgColor) + + # Setup the button frame and buttons + self.buttonFrame: tk.Frame = tk.Frame(content, height=0, background=elemBgColor, + highlightthickness=borderThickness, highlightbackground=borderColor, padx=padding, pady=padding) + self.loadButton: tk.Button = tk.Button(self.buttonFrame, height=1, width=buttonWidth, text="Load", font=fontStyle, background=buttonColor, + foreground=buttonFontColor, activebackground=buttonPressedColor, activeforeground=buttonFontColor, bd=0, + command=self.clickLoad) + self.saveButton: tk.Button = tk.Button(self.buttonFrame, height=1, width=buttonWidth, text="Save", font=fontStyle, background=buttonColor, + foreground=buttonFontColor, activebackground=buttonPressedColor, activeforeground=buttonFontColor, bd=0, + command=self.clickSave) + self.saveAsButton: tk.Button = tk.Button(self.buttonFrame, height=1, width=buttonWidth, text="Save As", font=fontStyle, background=buttonColor, + foreground=buttonFontColor, activebackground=buttonPressedColor, activeforeground=buttonFontColor, bd=0, + command=self.clickSaveAs) + self.resizeMap: tk.Button = tk.Button(self.buttonFrame, height=1, width=buttonWidth, text="Resize Map", font=fontStyle, background=buttonColor, + foreground=buttonFontColor, activebackground=buttonPressedColor, activeforeground=buttonFontColor, bd=0, + command=self.clickResizeMap) + self.exitButton: tk.Button = tk.Button(self.buttonFrame, height=1, width=buttonWidth, text="Exit", font=fontStyle, background=buttonColor, + foreground=buttonFontColor, activebackground=buttonPressedColor, activeforeground=buttonFontColor, bd=0, + command=self.clickExit) + + # Set up the canvasses + self.paletteCanvas: tk.Canvas = tk.Canvas( + content, background=elemBgColor, width=self.paletteCellSize * 3, height=self.paletteCellSize * 8, + highlightthickness=borderThickness, highlightbackground=borderColor) + self.mapCanvas: tk.Canvas = tk.Canvas( + content, background=elemBgColor, highlightthickness=borderThickness, highlightbackground=borderColor) + + # Set up the text + self.cellMetaData: tk.Text = tk.Text(content, width=10, state="disabled", + undo=True, autoseparators=True, maxundo=-1, + background=elemBgColor, foreground=fontColor, insertbackground=fontColor, font=fontStyle, + highlightthickness=borderThickness, highlightbackground=borderColor, + highlightcolor=borderHighlightColor, borderwidth=0, bd=0) + + self.scriptTextEntry: tk.Text = tk.Text(content, height=10, + undo=True, autoseparators=True, maxundo=-1, + background=elemBgColor, foreground=fontColor, insertbackground=fontColor, font=fontStyle, + highlightthickness=borderThickness, highlightbackground=borderColor, + highlightcolor=borderHighlightColor, borderwidth=0, bd=0) + + # Configure the main frame + content.grid(column=0, row=0, sticky=(tk.NSEW)) + frame.grid(column=0, row=0, columnspan=3, rowspan=4, sticky=(tk.NSEW)) + + # Place the button bar + self.buttonFrame.grid(column=0, row=0, columnspan=4, + rowspan=1, sticky=tk.NSEW, padx=padding, pady=padding) + + # Place the buttons in the button bar + self.loadButton.grid(column=0, row=0, sticky=tk.NW, + padx=padding, pady=padding) + self.saveButton.grid(column=1, row=0, sticky=tk.NW, + padx=padding, pady=padding) + self.saveAsButton.grid(column=2, row=0, sticky=tk.NW, + padx=padding, pady=padding) + self.resizeMap.grid(column=3, row=0, sticky=tk.NW, + padx=padding, pady=padding) + # Place this button in the top right + self.exitButton.grid(column=4, row=0, sticky=tk.NE, + padx=padding, pady=padding) + + # Configure button column weights + self.buttonFrame.columnconfigure(0, weight=0) + self.buttonFrame.columnconfigure(1, weight=0) + self.buttonFrame.columnconfigure(2, weight=0) + self.buttonFrame.columnconfigure(3, weight=1) + + # Place the palette and bind events + self.paletteCanvas.grid(column=0, row=1, sticky=( + tk.NSEW), padx=padding, pady=padding) + self.paletteCanvas.bind("", self.paletteLeftClick) + self.paletteCanvas.bind('', self.clickRelease) + self.paletteCanvas.bind('', self.paletteMouseMotion) + + # Place the map and bind events + self.mapCanvas.grid(column=1, row=1, rowspan=2, sticky=( + tk.NSEW), padx=padding, pady=padding) + self.mapCanvas.bind("", self.mapLeftClick) + self.mapCanvas.bind("", self.mapMiddleClick) + self.mapCanvas.bind("", self.mapRightClick) + self.mapCanvas.bind('', self.clickRelease) + self.mapCanvas.bind('', self.clickRelease) + self.mapCanvas.bind('', self.clickRelease) + self.mapCanvas.bind('', self.mapMouseMotion) + + # Place the cell metadata text window + self.cellMetaData.grid(column=2, row=1, rowspan=2, sticky=( + tk.NSEW), padx=padding, pady=padding) + + # Place the script editor text window + self.scriptTextEntry.grid(column=0, row=3, columnspan=3, sticky=( + tk.NSEW), padx=padding, pady=padding) + self.scriptTextEntry.bind("", self.scriptTextChanged) + + # Set root weights so the UI scales + self.root.columnconfigure(0, weight=1) + self.root.rowconfigure(0, weight=1) + + # Set row and column weights so the UI scales + content.columnconfigure(0, weight=0) + content.columnconfigure(1, weight=1) + content.columnconfigure(2, weight=0) + content.rowconfigure(0, weight=0) + content.rowconfigure(1, weight=1) + content.rowconfigure(2, weight=0) + content.rowconfigure(3, weight=0) + + self.texMapPalette: Mapping[tileType, ImageTk.PhotoImage] = {} + self.texMapMap: Mapping[tileType, ImageTk.PhotoImage] = {} + + self.loadTexture(self.texMapPalette, self.texMapMap, tileType.BG_FLOOR, '../../assets/ray/BG_FLOOR.png') + self.loadTexture(self.texMapPalette, self.texMapMap, tileType.BG_FLOOR_WATER, '../../assets/ray/BG_FLOOR_WATER.png') + self.loadTexture(self.texMapPalette, self.texMapMap, tileType.BG_FLOOR_LAVA, '../../assets/ray/BG_FLOOR_LAVA.png') + self.loadTexture(self.texMapPalette, self.texMapMap, tileType.BG_WALL_1, '../../assets/ray/BG_WALL_1.png') + self.loadTexture(self.texMapPalette, self.texMapMap, tileType.BG_WALL_2, '../../assets/ray/BG_WALL_2.png') + self.loadTexture(self.texMapPalette, self.texMapMap, tileType.BG_WALL_3, '../../assets/ray/BG_WALL_3.png') + self.loadTexture(self.texMapPalette, self.texMapMap, tileType.BG_DOOR, '../../assets/ray/BG_DOOR.png') + self.loadTexture(self.texMapPalette, self.texMapMap, tileType.BG_DOOR_CHARGE, '../../assets/ray/BG_DOOR_CHARGE.png') + self.loadTexture(self.texMapPalette, self.texMapMap, tileType.BG_DOOR_MISSILE, '../../assets/ray/BG_DOOR_MISSILE.png') + self.loadTexture(self.texMapPalette, self.texMapMap, tileType.BG_DOOR_ICE, '../../assets/ray/BG_DOOR_ICE.png') + self.loadTexture(self.texMapPalette, self.texMapMap, tileType.BG_DOOR_XRAY, '../../assets/ray/BG_DOOR_XRAY.png') + self.loadTexture(self.texMapPalette, self.texMapMap, tileType.BG_DOOR_SCRIPT, '../../assets/ray/BG_DOOR_SCRIPT.png') + self.loadTexture(self.texMapPalette, self.texMapMap, tileType.OBJ_ENEMY_START_POINT, 'imgs/OBJ_ENEMY_START_POINT.png') + self.loadTexture(self.texMapPalette, self.texMapMap, tileType.OBJ_ENEMY_NORMAL, '../../assets/ray/E_NORMAL_WALK_0.png') + self.loadTexture(self.texMapPalette, self.texMapMap, tileType.OBJ_ENEMY_STRONG, '../../assets/ray/E_STRONG_WALK_0.png') + self.loadTexture(self.texMapPalette, self.texMapMap, tileType.OBJ_ENEMY_ARMORED, '../../assets/ray/E_ARMORED_WALK_0.png') + self.loadTexture(self.texMapPalette, self.texMapMap, tileType.OBJ_ENEMY_FLAMING, '../../assets/ray/E_FLAMING_WALK_0.png') + self.loadTexture(self.texMapPalette, self.texMapMap, tileType.OBJ_ENEMY_HIDDEN, '../../assets/ray/E_HIDDEN_WALK_0.png') + self.loadTexture(self.texMapPalette, self.texMapMap, tileType.OBJ_ENEMY_BOSS, '../../assets/ray/E_BOSS_WALK_0.png') + self.loadTexture(self.texMapPalette, self.texMapMap, tileType.OBJ_ITEM_BEAM, '../../assets/ray/OBJ_ITEM_BEAM.png') + self.loadTexture(self.texMapPalette, self.texMapMap, tileType.OBJ_ITEM_CHARGE_BEAM, '../../assets/ray/OBJ_ITEM_CHARGE_BEAM.png') + self.loadTexture(self.texMapPalette, self.texMapMap, tileType.OBJ_ITEM_MISSILE, '../../assets/ray/OBJ_ITEM_MISSILE.png') + self.loadTexture(self.texMapPalette, self.texMapMap, tileType.OBJ_ITEM_ICE, '../../assets/ray/OBJ_ITEM_ICE.png') + self.loadTexture(self.texMapPalette, self.texMapMap, tileType.OBJ_ITEM_XRAY, '../../assets/ray/OBJ_ITEM_XRAY.png') + self.loadTexture(self.texMapPalette, self.texMapMap, tileType.OBJ_ITEM_SUIT_WATER, '../../assets/ray/OBJ_ITEM_SUIT_WATER.png') + self.loadTexture(self.texMapPalette, self.texMapMap, tileType.OBJ_ITEM_SUIT_LAVA, '../../assets/ray/OBJ_ITEM_SUIT_LAVA.png') + self.loadTexture(self.texMapPalette, self.texMapMap, tileType.OBJ_ITEM_ENERGY_TANK, '../../assets/ray/OBJ_ITEM_ENERGY_TANK.png') + self.loadTexture(self.texMapPalette, self.texMapMap, tileType.OBJ_ITEM_KEY, '../../assets/ray/OBJ_ITEM_KEY.png') + self.loadTexture(self.texMapPalette, self.texMapMap, tileType.OBJ_ITEM_ARTIFACT, '../../assets/ray/OBJ_ITEM_ARTIFACT.png') + self.loadTexture(self.texMapPalette, self.texMapMap, tileType.OBJ_ITEM_PICKUP_ENERGY, '../../assets/ray/OBJ_ITEM_PICKUP_ENERGY.png') + self.loadTexture(self.texMapPalette, self.texMapMap, tileType.OBJ_ITEM_PICKUP_MISSILE, '../../assets/ray/OBJ_ITEM_PICKUP_MISSILE.png') + self.loadTexture(self.texMapPalette, self.texMapMap, tileType.OBJ_SCENERY_TERMINAL, '../../assets/ray/OBJ_SCENERY_TERMINAL.png') + self.loadTexture(self.texMapPalette, self.texMapMap, tileType.DELETE, 'imgs/DELETE.png') + + # Start maximized + self.root.wm_state('normal') # 'zoomed' works for windows + + def loadTexture(self, pMap, mMap, key, texFile): + img = Image.open(texFile) + + # Resize for the palette + if(img.width < img.height): + newHeight = self.paletteCellSize + newWidth = img.width * (newHeight / img.height) + else: + newWidth = self.paletteCellSize + newHeight = img.height * (newWidth / img.width) + pResize = img.resize( + size=(int(newWidth), int(newHeight)), resample=Image.LANCZOS) + + pMap[key] = ImageTk.PhotoImage(pResize) + + # Resize for the map + if(img.width < img.height): + newHeight = self.mapCellSize + newWidth = img.width * (newHeight / img.height) + else: + newWidth = self.mapCellSize + newHeight = img.height * (newWidth / img.width) + + mResize = img.resize( + size=(int(newWidth), int(newHeight)), resample=Image.LANCZOS) + mMap[key] = ImageTk.PhotoImage(mResize) + + def setController(self, c): + from rme_controller import controller + self.c: controller = c + + def setModel(self, m): + from rme_model import model + self.m: model = m + + def mainloop(self): + self.root.mainloop() + + def paletteLeftClick(self, event: tk.Event): + x: int = self.paletteCanvas.canvasx(event.x) + y: int = self.paletteCanvas.canvasy(event.y) + self.c.clickPalette(int(x / self.paletteCellSize), + int(y / self.paletteCellSize)) + + def paletteMouseMotion(self, event: tk.Event): + x: int = self.paletteCanvas.canvasx(event.x) + y: int = self.paletteCanvas.canvasy(event.y) + self.c.moveMousePalette(int(x / self.paletteCellSize), + int(y / self.paletteCellSize)) + + def mapLeftClick(self, event: tk.Event): + x: int = self.mapCanvas.canvasx(event.x) + y: int = self.mapCanvas.canvasy(event.y) + self.c.leftClickMap(int(x / self.mapCellSize), + int(y / self.mapCellSize)) + + def mapRightClick(self, event: tk.Event): + self.isMapRightClicked = True + x: int = self.mapCanvas.canvasx(event.x) + y: int = self.mapCanvas.canvasy(event.y) + self.c.rightClickMap(int(x / self.mapCellSize), + int(y / self.mapCellSize)) + + def mapMiddleClick(self, event: tk.Event): + self.isMapMiddleClicked = True + self.mapCanvas.scan_mark(event.x, event.y) + + def mapMouseMotion(self, event: tk.Event): + if self.isMapMiddleClicked: + self.mapCanvas.scan_dragto(event.x, event.y, gain=1) + elif self.isMapRightClicked: + self.mapRightClick(event) + else: + x: int = self.mapCanvas.canvasx(event.x) + y: int = self.mapCanvas.canvasy(event.y) + self.c.moveMouseMap(int(x / self.mapCellSize), + int(y / self.mapCellSize)) + + def clickRelease(self, event: tk.Event): + self.isMapMiddleClicked = False + self.isMapRightClicked = False + self.c.releaseClick() + + def scriptTextChanged(self, event: tk.Event): + + self.m.setScripts(self.scriptTextEntry.get( + "1.0", tk.END).splitlines(keepends=False)) + + line = 1 + for script in self.m.scripts: + tag: str = 'highlight' + str(line) + self.scriptTextEntry.tag_remove( + tag, str(line) + '.0', str(line) + '.0 lineend') + self.scriptTextEntry.tag_add( + tag, str(line) + '.0', str(line) + '.0 lineend') + if script.isValid(): + self.scriptTextEntry.tag_configure( + tag, background="green", foreground="black") + else: + self.scriptTextEntry.tag_configure( + tag, background="red", foreground="black") + line = line + 1 + + def redraw(self): + self.paletteCanvas.delete('all') + self.mapCanvas.delete('all') + + # Draw backgrounds in the palette + y: int = 0 + for bg in bgTiles: + if bg is not tileType.EMPTY: + self.paletteCanvas.create_image( + 0, y*self.paletteCellSize, image=self.texMapPalette[bg], anchor=tk.NW) + y = y+1 + + # Draw objects in the palette into two columns + x: int = 1 + y: int = 0 + for obj in objTiles: + if obj is not tileType.EMPTY: + + imgWidth: int = self.texMapPalette[obj].width() + hOffset = int((self.paletteCellSize - imgWidth) / 2) + + self.paletteCanvas.create_image( + x*self.paletteCellSize + hOffset, y*self.paletteCellSize, image=self.texMapPalette[obj], anchor=tk.NW) + y = y+1 + if NUM_PALETTE_ROWS == y: + y = 0 + x = x+1 + + # Draw the map + for x in range(self.m.getMapWidth()): + for y in range(self.m.getMapHeight()): + self.drawMapCell(x, y) + + # Clear highlight from old cell + self.mapCanvas.delete(self.highlightRect) + # Highlight new cell + self.highlightRect = self.mapCanvas.create_rectangle( + (self.selRectX * self.mapCellSize), (self.selRectY * self.mapCellSize), ((self.selRectX + 1) * self.mapCellSize), ((self.selRectY + 1) * self.mapCellSize), outline='yellow') + + def drawMapCell(self, x, y): + t: tile = self.m.tileMap[x][y] + if (t.background is not tileType.EMPTY): + imgWidth: int = self.texMapMap[t.background].width() + hOffset = int((self.mapCellSize - imgWidth) / 2) + + self.mapCanvas.create_image( + (x * self.mapCellSize) + hOffset, (y * self.mapCellSize), image=self.texMapMap[t.background], anchor=tk.NW) + if (t.object is not tileType.EMPTY): + imgWidth: int = self.texMapMap[t.object].width() + hOffset = int((self.mapCellSize - imgWidth) / 2) + + self.mapCanvas.create_image( + (x * self.mapCellSize) + hOffset, (y * self.mapCellSize), image=self.texMapMap[t.object], anchor=tk.NW) + + def drawSelectedTile(self, selectedTile: tileType, x, y): + # Clear highlight from old cell + self.paletteCanvas.delete(self.paletteHighlightRect) + # Highlight new cell + self.paletteHighlightRect = self.paletteCanvas.create_rectangle( + (x * self.paletteCellSize), (y * self.paletteCellSize), ((x + 1) * self.paletteCellSize), ((y + 1) * self.paletteCellSize), outline='yellow', width=5) + pass + + def selectCell(self, x, y, objId): + self.selRectX = x + self.selRectY = y + + # Clear highlight from old cell + self.mapCanvas.delete(self.highlightRect) + # Highlight new cell + self.highlightRect = self.mapCanvas.create_rectangle( + (x * self.mapCellSize), (y * self.mapCellSize), ((x + 1) * self.mapCellSize), ((y + 1) * self.mapCellSize), outline='yellow') + + # Enable the text for writing + self.cellMetaData.configure(state='normal') + + # Delete is going to erase anything + # in the range of 0 and end of file, + # The respective range given here + self.cellMetaData.delete('1.0', 'end') + + # Insert method inserts the text at + # specified position, Here it is the + # beginning + self.cellMetaData.insert( + '1.0', "{" + str(x) + "." + str(y) + "}") + if objId >= 0: + self.cellMetaData.insert( + '2.0', "\nID: " + str(objId)) + + # Disable the text for writing + self.cellMetaData.configure(state='normal') + + def clickSave(self): + if self.currentFilePath is None: + self.clickSaveAs() + else: + with open(self.currentFilePath, 'wb') as saveFile: + self.m.save(saveFile) + + def clickSaveAs(self): + fts = ( + ('Ray Map Data', '*.rmd'), + ('All files', '*.*') + ) + saveFile: TextIOWrapper = asksaveasfile( + mode='wb', filetypes=fts, defaultextension='rmd') + if saveFile is not None: + self.currentFilePath = os.path.abspath(saveFile.name) + self.m.save(saveFile) + + def clickLoad(self): + fts = ( + ('Ray Map Data', '*.rmd'), + ('All files', '*.*') + ) + loadFile: TextIOWrapper = askopenfile(mode='rb', filetypes=fts) + if loadFile is not None: + self.currentFilePath = os.path.abspath(loadFile.name) + self.m.load(loadFile) + + # Redraw map + self.redraw() + + # Clear and set script text + self.scriptTextEntry.delete('1.0', tk.END) + for script in self.m.scripts: + self.scriptTextEntry.insert(tk.END, script.toString() + '\n') + # Highlight text + self.scriptTextChanged(None) + + def clickResizeMap(self): + inputStr: str = str(self.m.getMapWidth()) + 'x' + \ + str(self.m.getMapHeight()) + validInput: bool = True + while True: + if validInput: + inputStr = simpledialog.askstring( + 'Map Size', 'Enter the map size (w x h)', initialvalue=inputStr) + else: + inputStr = simpledialog.askstring( + 'Map Size', 'Invalid value\nEnter the map size (w x h)', initialvalue=inputStr) + + if None is inputStr: + # Cancel pressed + return + + # Validate value + try: + # Pick out the dimensions + parts: list[str] = inputStr.split('x') + newW: int = int(parts[0].strip()) + newH: int = int(parts[1].strip()) + if 0 < newW and newW < 256 and 0 < newH and newH < 256: + # Resize the map + self.m.setMapSize(newW, newH) + # Redraw map and revalidate scripts + self.redraw() + self.scriptTextChanged(None) + return + else: + validInput = False + except: + # Validation failed, try again + validInput = False + + def clickExit(self): + if self.currentFilePath is not None: + self.clickSave() + else: + with open('autosave.rmd', 'wb') as outFile: + self.m.save(outFile) + self.root.destroy() diff --git a/tools/reboot_into_bootloader/.gitignore b/tools/reboot_into_bootloader/.gitignore new file mode 100644 index 000000000..500a9e681 --- /dev/null +++ b/tools/reboot_into_bootloader/.gitignore @@ -0,0 +1 @@ +reboot_swadge_into_bootloader \ No newline at end of file diff --git a/tools/soko/plugin/sokobon_binary_conversion_script.lua b/tools/soko/plugin/sokobon_binary_conversion_script.lua new file mode 100644 index 000000000..b6e69191a --- /dev/null +++ b/tools/soko/plugin/sokobon_binary_conversion_script.lua @@ -0,0 +1,91 @@ +-- Script to export tilemap data as a binary file. +-- Original script by Zeltrix (https://pastebin.com/mQGiKAgR) +-- Export to binary by JVeg199X +-- Note: This script only works with tilemaps of 255x255 tiles or less + +-- DO NOT USE. WIP for original binary +-- Check .asp file and .config file + +if TilesetMode == nil then return app.alert "Use Aseprite 1.3" end +local spr = app.activeSprite + +if not spr then return end + +-- TODO Add Multi-File Selection for multiple levels and config files + +local d = Dialog("Export Tilemap as .bin File") +d:label{id="lab1",label="",text="Export Tilemap as .bin File for your own GameEngine"} + :file{id = "path", label="Export Path", filename="",open=false,filetypes={"bin"}, save=true, focus=true} + :label{id="lab3", label="",text="Max supported tilemap size: 255x255"} + :separator{} + :label{id="lab2", label="",text="In the last row of the tilemap-layer there has to be at least one Tile \"colored\" to fully export the whole Tilemap"} + :button{id="ok",text="&OK",focus=true} + :button{text="&Cancel" } + :show() + + + +--Initialize warp data array +local warps = {} +for i=0, 15 do + warps[i] = {} + warps[i][0] = 0; + warps[i][1] = 0; +end + +local data = d.data +if not data.ok then return end + local lay = app.activeLayer + if(#data.path<=0)then app.alert("No path selected") end + if not lay.isTilemap then return app.alert("Layer is not tilemap") end + pc = app.pixelColor + mapFile = io.open(data.path,"w") + + for _,c in ipairs(lay.cels) do + local img = c.image + + --The first two bytes contain the width and height of the tilemap in tiles + mapFile:write(string.char(img.width)) + mapFile:write(string.char(img.height)) + + --The next section of bytes is the tilemap itself + for p in img:pixels() do + if(p ~= nil) then + local tileId = p() + + --if(tileId == 130) then + -- local d2 = Dialog(tileId) + -- d2:show() + --end + + if(tileId > 0 and tileId < 17) then + --warp tiles + + tileBelowCurrentTile = img:getPixel(p.x, p.y+1) + if(tileBelowCurrentTile == 34 or tileBelowCurrentTile == 64 or tileBelowCurrentTile == 158) then + --if tile below warp tile is brick block or container or checkpoint, write it like normal + mapFile:write(string.char(tileId)) + else + --otherwise store it in warps array and don't write it into the file just yet + warps[tileId-1][0] = p.x + warps[tileId-1][1] = p.y + mapFile:write(string.char(0)) + end + + else + --every other tile + mapFile:write(string.char(tileId)) + end + + end + end + + --The last 32 bytes are warp x and y locations + for i=0, 15 do + mapFile:write(string.char(warps[i][0])) + mapFile:write(string.char(warps[i][1])) + end + end + + mapFile:close() + \ No newline at end of file diff --git a/tools/soko/templateTiledProject/README.md b/tools/soko/templateTiledProject/README.md new file mode 100644 index 000000000..291705d94 --- /dev/null +++ b/tools/soko/templateTiledProject/README.md @@ -0,0 +1,62 @@ +Open the project 'templateProject.tiled-project' using the most recent version of Tiled tilemap editor. + +## IF YOUR OBJECTS DO NOT SNAP TO THE CENTER OF THE GRID TILES, GO TO EDIT>PREFERENCE>FINE GRID DIVISIONS AND SET IT TO 2. + +### All tiles should go in the 'tiles' tilemap layer. Use the 'tilesheet' tileset to place walls, floors, and goals. +### All entities should go in the 'entities' object layer. Use the 'objLayers' tileset to place objects with baked-in data. + +### Level List File +The game uses an overworld for level selection. In order to designate the level to be loaded, an index number should be provided. Please prefix your level binary 'sk_' and end it with '.bin'. The former prevents filename collisions and the latter is mandatory to be properly copied into system memory. The 'SK_LEVEL_LIST.txt' file should be edited to include the desired index and name of your level. The level list file is formatted as such: +``` +1:sk_overworld.bin: +7:sk_test1.bin: +8:sk_test2.bin: +9:sk_test3.bin: +2:sk_warehouse.bin: +``` +## Entities: + +### Player: + Be sure to set the 'gamemode' property. + Valid values are: + SOKO_OVERWORLD, + SOKO_CLASSIC, + SOKO_EULER, + SOKO_LASERBOUNCE + +### Crate: + The 'sticky' property indicates whether the crate will stick to a player's sprite. + The 'trail' property indicates whether a crate will leave its own trail in a SOKO_EULER puzzle. + +### Button: + The 'playerPress' property indicates whether a player can depress the button. + The 'cratePress' property indicates whether a crate can depress the button. + The 'invertAction' property inverts the button's effects on all of its target blocks. For instance, all non-inverted Ghost Blocks targeted by the Button will start intangible. + The 'stayDownOnPress' property indicates whether the button will remain depressed after its first press after resets once players or crates are removed. + To target a ghostblock, find the Object ID of the target in Tiled and populate the target#id property with that ID (start at target1id and count up). + Be sure to set the 'numTargets' property to the number of targeted blocks. + +### Ghost Block: + Target a Ghost Block with a Button. + The 'playerMove' property indicates whether a player can move the ghost block like a crate while in its tangible state. + The 'inverted' property indicates whether a Ghost block will start intangible (unless the button targeting it is intangible). + +### Internal Warp and Internal Warp Exit: + The 'hp' property indicates how many times a Warp can be entered. + The 'allow_crates' property indicates whether an Internal Warp may pass crates to their destination. Note that the destination will be blocked by a Crate on its destination. + To target another internal warp, find the Object ID of the target in Tiled and populate the 'target_id' field with that ID. + Warps may only target Internal Warp and Internal Warp Exit blocks. Internal Warp Exits have no function in gameplay and serve only as destination markers for Internal Warps. To make a 2-Way Portal, have two Internal Warps target one another's IDs. + +### External Warps: + External warps are used in the overworld for level selection. When the player steps on an External Warp, the level pointed to by the associated index (See Level List File) will be loaded. When the player completes the loaded puzzle, they will automatically reload the overworld level they came from. + The 'manuallyIndexed' property, when true, indicates that the game should check the 'target_id' value to find the appropriate level index. When false, this property indicates that the game may use this warp to point to a level which is not already attached to another external warp. Automatically indexed external warps will be assigned the lowest unused level index from the Level List File. + +### Laser Emitter/Receiver: + Be sure to set the 'emitDirection' property. + Valid values are: + UP, + DOWN, + RIGHT, + LEFT + The 'playerMove' property indicates where the Laser Emitter can be pushed by players. + diff --git a/tools/soko/templateTiledProject/entitySprites/button16.png b/tools/soko/templateTiledProject/entitySprites/button16.png new file mode 100644 index 000000000..2065777b1 Binary files /dev/null and b/tools/soko/templateTiledProject/entitySprites/button16.png differ diff --git a/tools/soko/templateTiledProject/entitySprites/crate16.png b/tools/soko/templateTiledProject/entitySprites/crate16.png new file mode 100644 index 000000000..aac13c443 Binary files /dev/null and b/tools/soko/templateTiledProject/entitySprites/crate16.png differ diff --git a/tools/soko/templateTiledProject/entitySprites/ghostblock16.png b/tools/soko/templateTiledProject/entitySprites/ghostblock16.png new file mode 100644 index 000000000..cef0a40c9 Binary files /dev/null and b/tools/soko/templateTiledProject/entitySprites/ghostblock16.png differ diff --git a/tools/soko/templateTiledProject/entitySprites/laser90Right16.png b/tools/soko/templateTiledProject/entitySprites/laser90Right16.png new file mode 100644 index 000000000..f620ba500 Binary files /dev/null and b/tools/soko/templateTiledProject/entitySprites/laser90Right16.png differ diff --git a/tools/soko/templateTiledProject/entitySprites/laserEmitUp16.png b/tools/soko/templateTiledProject/entitySprites/laserEmitUp16.png new file mode 100644 index 000000000..d73995747 Binary files /dev/null and b/tools/soko/templateTiledProject/entitySprites/laserEmitUp16.png differ diff --git a/tools/soko/templateTiledProject/entitySprites/laserReceiveOmni16.png b/tools/soko/templateTiledProject/entitySprites/laserReceiveOmni16.png new file mode 100644 index 000000000..a12ad9b32 Binary files /dev/null and b/tools/soko/templateTiledProject/entitySprites/laserReceiveOmni16.png differ diff --git a/tools/soko/templateTiledProject/entitySprites/laserReceiveUp16.png b/tools/soko/templateTiledProject/entitySprites/laserReceiveUp16.png new file mode 100644 index 000000000..1a7283a84 Binary files /dev/null and b/tools/soko/templateTiledProject/entitySprites/laserReceiveUp16.png differ diff --git a/tools/soko/templateTiledProject/entitySprites/player16.png b/tools/soko/templateTiledProject/entitySprites/player16.png new file mode 100644 index 000000000..87daee395 Binary files /dev/null and b/tools/soko/templateTiledProject/entitySprites/player16.png differ diff --git a/tools/soko/templateTiledProject/entitySprites/warpexternal16.png b/tools/soko/templateTiledProject/entitySprites/warpexternal16.png new file mode 100644 index 000000000..d622ba0f6 Binary files /dev/null and b/tools/soko/templateTiledProject/entitySprites/warpexternal16.png differ diff --git a/tools/soko/templateTiledProject/entitySprites/warpinternal16.png b/tools/soko/templateTiledProject/entitySprites/warpinternal16.png new file mode 100644 index 000000000..3ac464870 Binary files /dev/null and b/tools/soko/templateTiledProject/entitySprites/warpinternal16.png differ diff --git a/tools/soko/templateTiledProject/entitySprites/warpinternalexit16.png b/tools/soko/templateTiledProject/entitySprites/warpinternalexit16.png new file mode 100644 index 000000000..b785f3666 Binary files /dev/null and b/tools/soko/templateTiledProject/entitySprites/warpinternalexit16.png differ diff --git a/tools/soko/templateTiledProject/extensions/export-to-soko.js b/tools/soko/templateTiledProject/extensions/export-to-soko.js new file mode 100644 index 000000000..78a111d0f --- /dev/null +++ b/tools/soko/templateTiledProject/extensions/export-to-soko.js @@ -0,0 +1,391 @@ +var customMapFormat = { + name: "Swadge Sokobon Level Format", + extension: "bin", + write: + + function(p_map,p_fileName) { + + //Special Characters + var sokoSigs = + { + stackInPlace: 201, + compress: 202, + player: 203, + crate: 204, + warpinternal: 205, + warpinternalexit: 206, + warpexternal: 207, + button: 208, + laserEmitUp: 209, + laserReceiveOmni: 210, + laserReceiveUp: 211, + laser90Right: 212, + ghostblock: 213, + stackObjEnd: 230 + } + + var m = { + width: p_map.width, + height: p_map.height, + layers: [] + }; + + var sokoTileLayer, sokoObjectLayer; + var objArr = []; + //tiled.time("Export completed in"); + for (var i = 0; i < p_map.layerCount; ++i) + { + var layer = p_map.layerAt(i); + if(layer.isTileLayer) + { + sokoTileLayer = layer; + tiled.log("Layer " + i + " is Tile Layer"); + } + if(layer.isObjectLayer) + { + sokoObjectLayer = layer; + tiled.log("Layer " + i + " is Object Layer"); + } + } + sokoObjectLayer.objects.forEach( function(arrItem, ind) + { + tiled.log(ind); + + tiled.log(arrItem.tile.className); + var xval = Math.round(arrItem.x / arrItem.width); + var yval = Math.round(arrItem.y / arrItem.height - 1); + var posit = xval + yval * sokoTileLayer.width; + + tiled.log("(" + xval + "," + yval + ") Pos: " + posit + "(Width: " + sokoTileLayer.width + ")"); + var props = arrItem.resolvedProperties(); + tiled.log(JSON.stringify(props)) + tiled.log("-------------"); + var objItem = + { + obj: arrItem, + pos: posit, + x: xval, + y: yval, + index: ind + }; + objArr.push(objItem); + } + + + + ) + objArr.sort((a,b)=>(a.pos > b.pos) ? -1 : 1); //Sort by index in descending order so we can just split and insert stacked objects + objArr.forEach( function(arrItem) + { + tiled.log("Index:" + arrItem.index + ";Pos:" + arrItem.pos + ":(" + arrItem.x + ","+arrItem.y + ")"); + } + ) + for (var i = 0; i < p_map.layerCount; ++i) + { + var layer = p_map.layerAt(i); + + if(!layer.isTileLayer) + { + continue; + } + + data = []; + if(layer.isTileLayer) { + data.push(layer.width); + data.push(layer.height); + var rows = []; + for (y = 0; y < layer.height; ++y) { + var row = []; + for (x = 0; x < layer.width; ++x) + { + row.push(layer.cellAt(x,y).tileId); + data.push(layer.cellAt(x,y).tileId+1); + } + rows.push(row); + } + + //PackInObjects + if(1) + { + objArr.forEach(function(objItem, ind, objArr) + { + headerOffset = 2; + var objClassName = objItem.obj.tile.className; + tiled.log(sokoSigs[objClassName]); + var propertyVals = [111]; + propertyVals = propExtract(objItem, objArr); + insertionData = [sokoSigs.stackInPlace, sokoSigs[objClassName]].concat(propertyVals).concat([sokoSigs.stackObjEnd]); + tiled.log("DataBefore: " + data.slice(0,objItem.pos+headerOffset+1)); + tiled.log("InsertionData: " + insertionData); + tiled.log("DataAfter: " + data.slice(objItem.pos+headerOffset+1)); + data = data.slice(0,objItem.pos+headerOffset+1).concat(insertionData).concat(data.slice(objItem.pos+headerOffset+1)); + } + + ) + } + m.layers.push(rows); + tiled.log(m.layers); + //var file = new TextFile(fileName, TextFile.WriteOnly); + tiled.log("Export to " + p_fileName); + let view = Uint8Array.from(data); + let fileHand = new BinaryFile(p_fileName, BinaryFile.WriteOnly); + let buffer = view.buffer.slice(view.byteOffset, view.byteLength + view.byteOffset); + //let buffer = view.buffer; + tiled.log(view); + fileHand.write(buffer); + fileHand.commit(); + tiled.log(buffer); + } + } + + + } + + +} + +function findObjCoordById(objArr,id) +{ + var loopArgs = + { + id: id, + retVal: { + x: 0, + y: 0, + valid: false, + index: 0 + } + } + objArr.forEach( function(objEntry, ind, arr){ + //tiled.log("Target ID: " + this.id + " Entry ID: " + objEntry.obj.id + " Pos:(" + objEntry.x + "," + objEntry.y + ")"); + + if(this.id == objEntry.obj.id) + { + //tiled.log("MATCH!"); + this.retVal = { + x: objEntry.x, + y: objEntry.y, + valid: true, + index: ind + }; + } + + } , loopArgs + ) + return loopArgs.retVal; +} + +function propExtract(objItem, objArr) +{ + + soko_direction = + { + UP: 0, + DOWN: 1, + RIGHT: 2, + LEFT: 3 + }; + soko_player_gamemodes = + { + SOKO_OVERWORLD: 0, + SOKO_CLASSIC: 1, + SOKO_EULER: 2, + SOKO_LASERBOUNCE: 3 + }; + soko_crate_properties = + { + sticky: 0b1, + trail: 0b10 + }; + soko_warpinternal_properties = + { + allow_crates: 0b1 + }; + soko_warpexternal_properties = + { + manualIndex: 0b1 + }; + soko_laser90Right_properties = + { + emitDirection: 0b1, + playerMove: 0b10 + }; + soko_laserEmitUp_properties = + { + playerMove: 0b10 + }; + soko_button_properties = + { + cratePress: 0b1, + playerPress: 0b10, + invertAction: 0b100, + stayDownOnPress: 0b1000 + }; + soko_ghostblock_properties = + { + inverted: 0b100, + playerMove: 0b10 + }; + + var properties = objItem.obj.resolvedProperties(); + + retVal = []; + + switch(objItem.obj.tile.className) + { + case "player": + retVal.push(soko_player_gamemodes[properties.gamemode]); + break; + case "crate": + var variant = 0b0; + if(properties.sticky) + { + variant = variant | soko_crate_properties.sticky; + } + if(properties.trail) + { + variant = variant | soko_crate_properties.trail; + } + retVal.push(variant); + break; + case "laser90Right": + var variant = 0b0; + if(properties.emitDirection) + { + variant = variant | soko_laser90Right_properties.emitDirection; + //tiled.log("laser90Right:emitDirection:" + properties.emitDirection); + } + if(properties.playerMove) + { + variant = variant | soko_laser90Right_properties.playerMove; + //tiled.log("laser90Right:emitDirection:" + properties.playerMove); + } + retVal.push(variant); + break; + case "laserEmitUp": + var variant = 0b0; + if(properties.playerMove) + { + variant = variant | soko_laserEmitUp_properties.playerMove; + } + //tiled.log("" + properties.emitDirection + " " + soko_direction[properties.emitDirection]); + variant = variant | ((soko_direction[properties.emitDirection] & 0b11) << 6); + retVal.push(variant); + break; + case "laserReceiveUp": + var variant = 0b0; + tiled.log("" + properties.emitDirection + " " + soko_direction[properties.emitDirection]); + variant = variant | ((soko_direction[properties.emitDirection] & 0b11) << 6); + retVal.push(variant); + break + case "warpinternal": + var variant = 0b0; + if(properties.allow_crates) + { + variant = variant | soko_warpinternal_properties.allow_crates; + //tiled.log("laser90Right:emitDirection:" + properties.emitDirection); + } + retVal.push(variant); + retVal.push(properties.hp); + var targetCoord = findObjCoordById(objArr,properties.target_id); + //if(targetCoord.valid) + //{ + //tiled.log("className === warpinternalexit || className === warpinternal: " + ((objArr[targetCoord.index].obj.tile.className === "warpinternalexit") || (objArr[targetCoord.index].obj.tile.className === "warpinternal"))); + //} + if(!targetCoord.valid) + { + tiled.log("No Valid Warp Exit at target_id"); + } + if(targetCoord.valid && ((objArr[targetCoord.index].obj.tile.className === "warpinternalexit") || (objArr[targetCoord.index].obj.tile.className === "warpinternal"))){ + tiled.log("Warp Valid ID: " + properties.target_id + " at Coord(" + targetCoord.x + "," + targetCoord.y + ")"); + //retVal.push(properties[idString]); + retVal.push(targetCoord.x); + retVal.push(targetCoord.y); + } + break; + case "warpexternal": + var variant = 0b0; + var target_id = properties.target_id; + if(properties.manualIndex) + { + variant = variant | soko_warpexternal_properties.manualIndex; + //tiled.log("laser90Right:emitDirection:" + properties.emitDirection); + } + retVal.push(variant); + retVal.push(target_id); + break; + case "button": + var variant = 0b0; + if(properties.cratePress) + { + variant = variant | soko_button_properties.cratePress; + } + if(properties.invertAction) + { + variant = variant | soko_button_properties.invertAction; + } + if(properties.playerPress) + { + variant = variant | soko_button_properties.playerPress; + } + if(properties.stayDownOnPress) + { + variant = variant | soko_button_properties.stayDownOnPress; + } + var numTarg = (properties.numTargets & 0b111); + variant = variant | (numTarg << 5); //store the number of targets in the upper 5 bits (up to 7 targets per button) + retVal.push(variant); + for(var i = 0; i < numTarg; ++i) + { + idString = "target" + (i+1) + "id"; + var targetCoord = findObjCoordById(objArr,properties[idString]); + if(targetCoord.valid){ + tiled.log("Valid ID:" + properties[idString] + " at Coord(" + targetCoord.x + "," + targetCoord.y + ")"); + //retVal.push(properties[idString]); + retVal.push(targetCoord.x); + retVal.push(targetCoord.y); + } + else + { + numTarg -= 1; //discard invalid target ID, reduce target count by 1 + variant = variant & 0b11111; + variant = variant | (numTarg << 5); + retVal[0] = variant; + } + } + break; + case "ghostblock": + var variant = 0b0; + if(properties.inverted) + { + variant = variant | soko_ghostblock_properties.inverted; + } + if(properties.playerMove) + { + variant = variant | soko_ghostblock_properties.playerMove; + } + + } + return retVal; +} + +tiled.log("Registering Soko Map Export"); +//tiled.log(tiled.activeAsset.layers[0].cellAt(3,1)); +//map = tiled.activeAsset; +//dat = []; +//dat.push(map.width); +//dat.push(map.height); +//for (var y = 0; y < map.height; ++y) +/* +{ + for(var x = 0; x + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tools/soko/templateTiledProject/sk_binOverworld.bin b/tools/soko/templateTiledProject/sk_binOverworld.bin new file mode 100644 index 000000000..9aab206e2 Binary files /dev/null and b/tools/soko/templateTiledProject/sk_binOverworld.bin differ diff --git a/tools/soko/templateTiledProject/sk_binOverworld.tmx b/tools/soko/templateTiledProject/sk_binOverworld.tmx new file mode 100644 index 000000000..c699de021 --- /dev/null +++ b/tools/soko/templateTiledProject/sk_binOverworld.tmx @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + +12,12,12,12,12,12,12,12,12, +12,13,13,13,13,13,13,13,12, +12,13,13,13,13,13,13,13,12, +12,13,13,13,13,13,13,13,12, +12,13,13,13,13,13,13,13,12, +12,13,13,13,13,13,13,13,12, +12,13,13,13,13,13,13,13,12, +12,13,13,13,13,13,13,13,12, +12,12,12,12,12,12,12,12,12 + + + + + + + + + + + + + + diff --git a/tools/soko/templateTiledProject/sk_laserTest.bin b/tools/soko/templateTiledProject/sk_laserTest.bin new file mode 100644 index 000000000..b0208cc4b Binary files /dev/null and b/tools/soko/templateTiledProject/sk_laserTest.bin differ diff --git a/tools/soko/templateTiledProject/sk_laserTest.tmx b/tools/soko/templateTiledProject/sk_laserTest.tmx new file mode 100644 index 000000000..9623171d1 --- /dev/null +++ b/tools/soko/templateTiledProject/sk_laserTest.tmx @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + +12,12,12,12,12,12,12,12,12, +12,13,13,13,13,13,13,13,12, +12,13,13,13,13,13,13,13,12, +12,13,13,13,13,13,13,13,12, +12,13,13,13,13,13,13,13,12, +12,13,13,13,13,13,13,13,12, +12,13,13,13,13,13,13,13,12, +12,13,13,13,13,13,13,13,12, +12,12,12,12,12,12,12,12,12 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tools/soko/templateTiledProject/templateMap.bin b/tools/soko/templateTiledProject/templateMap.bin new file mode 100644 index 000000000..cc7f267ef Binary files /dev/null and b/tools/soko/templateTiledProject/templateMap.bin differ diff --git a/tools/soko/templateTiledProject/templateMap.tmx b/tools/soko/templateTiledProject/templateMap.tmx new file mode 100644 index 000000000..be9e1a588 --- /dev/null +++ b/tools/soko/templateTiledProject/templateMap.tmx @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + +12,12,12,12,12,12,12,12, +12,13,13,14,13,13,13,12, +12,13,13,13,13,13,13,12, +12,13,13,13,13,13,15,12, +12,13,13,13,13,13,13,12, +12,12,12,12,12,12,12,12 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tools/soko/templateTiledProject/templateProject.tiled-project b/tools/soko/templateTiledProject/templateProject.tiled-project new file mode 100644 index 000000000..d0eb59206 --- /dev/null +++ b/tools/soko/templateTiledProject/templateProject.tiled-project @@ -0,0 +1,14 @@ +{ + "automappingRulesFile": "", + "commands": [ + ], + "compatibilityVersion": 1100, + "extensionsPath": "extensions", + "folders": [ + "." + ], + "properties": [ + ], + "propertyTypes": [ + ] +} diff --git a/tools/soko/templateTiledProject/templateProject.tiled-session b/tools/soko/templateTiledProject/templateProject.tiled-session new file mode 100644 index 000000000..ea98e1286 --- /dev/null +++ b/tools/soko/templateTiledProject/templateProject.tiled-session @@ -0,0 +1,42 @@ +{ + "activeFile": "templateMap.tmx", + "expandedProjectPaths": [ + "." + ], + "file.lastUsedOpenFilter": "All Files (*)", + "fileStates": { + "": { + "scaleInEditor": 1 + }, + "objLayers.tsx": { + "dynamicWrapping": true, + "scaleInDock": 1, + "scaleInEditor": 1 + }, + "templateMap.tmx": { + "scale": 4.492708333333334, + "selectedLayer": 1, + "viewCenter": { + "x": 58.094134013447714, + "y": 48.18919545559935 + } + }, + "templateMap.tmx#tilesheet": { + "dynamicWrapping": false, + "scaleInDock": 1 + } + }, + "last.exportedFilePath": "C:/Users/grana/repos/Swadge-IDF-5.0/tools/soko/templateTiledProject", + "last.imagePath": "C:/Users/grana/repos/Swadge-IDF-5.0/tools/soko/templateTiledProject/tileSprites", + "map.lastUsedExportFilter": "All Files (*)", + "openFiles": [ + "templateMap.tmx", + "objLayers.tsx" + ], + "project": "templateProject.tiled-project", + "property.type": "bool", + "recentFiles": [ + "objLayers.tsx", + "templateMap.tmx" + ] +} diff --git a/tools/soko/templateTiledProject/tileSprites/tilesheet.png b/tools/soko/templateTiledProject/tileSprites/tilesheet.png new file mode 100644 index 000000000..90c89a827 Binary files /dev/null and b/tools/soko/templateTiledProject/tileSprites/tilesheet.png differ diff --git a/tools/soko/templateTiledProject/warehouse.bin b/tools/soko/templateTiledProject/warehouse.bin new file mode 100644 index 000000000..216b97486 Binary files /dev/null and b/tools/soko/templateTiledProject/warehouse.bin differ diff --git a/tools/soko/templateTiledProject/warehouse.tmx b/tools/soko/templateTiledProject/warehouse.tmx new file mode 100644 index 000000000..328d85892 --- /dev/null +++ b/tools/soko/templateTiledProject/warehouse.tmx @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + +0,0,12,12,12,12,12,0, +12,12,12,13,13,13,12,0, +12,14,13,13,13,13,12,0, +12,12,12,13,13,14,12,0, +12,14,12,12,13,13,12,0, +12,13,12,13,14,13,12,12, +12,13,13,14,13,13,14,12, +12,13,13,13,14,13,13,12, +12,12,12,12,12,12,12,12 + + + + + + + + + + + + + diff --git a/tools/soko/tmx_to_binary.py b/tools/soko/tmx_to_binary.py new file mode 100644 index 000000000..47c948b3d --- /dev/null +++ b/tools/soko/tmx_to_binary.py @@ -0,0 +1,39 @@ +import tkinter as tk +from tkinter import filedialog +from xml.dom.minidom import parse,parseString + +SIG_BYTE_SIMPLE_SEQUENCE = 200 #This byte will capture long strings of bytes in compact form. Example [12][12][12]...[12] => [200][#][12] where # is number of 12 tiles + +def insert_position(position, sourceList, insertionList): + return sourceList[:position] + insertionList + sourceList[position:] + +root = tk.Tk() +root.withdraw() + +file_path = filedialog.askopenfilename() + +print(file_path) + +document = parse(file_path) + +mapHeaderWidth = int(document.getElementsByTagName("map")[0].attributes.getNamedItem("width").nodeValue) +print(mapHeaderWidth) +mapHeaderHeight = int(document.getElementsByTagName("map")[0].attributes.getNamedItem("height").nodeValue) +print(mapHeaderHeight) +dataText = document.getElementsByTagName("data")[0].firstChild.nodeValue +print(dataText) +scrub = "".join(dataText.split()) #Remove all residual whitespace in data block +scrub2 = [int(i) for i in scrub.split(",")] #Convert all tileIDs to int. +print(scrub) +print(scrub2) +#scrub3 = list([mapHeaderWidth,mapHeaderHeight]) + scrub2 +scrub3 = insert_position(0,scrub2,list([mapHeaderWidth,mapHeaderHeight])) +print(scrub3) +rawBytes = bytearray(scrub3) +rawBytesImmut = bytes(rawBytes) +outfile_path = "".join([file_path.split(".")[0],".bin"]) +print(outfile_path) +with open(outfile_path,"wb") as binary_file: + binary_file.write(rawBytesImmut) + + diff --git a/tools/spiffs_file_preprocessor/src/rmd_processor.c b/tools/spiffs_file_preprocessor/src/rmd_processor.c new file mode 100644 index 000000000..37993bd40 --- /dev/null +++ b/tools/spiffs_file_preprocessor/src/rmd_processor.c @@ -0,0 +1,40 @@ +#include +#include +#include +#include + +#include "rmd_processor.h" +#include "heatshrink_encoder.h" +#include "fileUtils.h" +#include "heatshrink_util.h" + +void process_rmd(const char* infile, const char* outdir) +{ + /* Determine if the output file already exists */ + char outFilePath[128] = {0}; + strcat(outFilePath, outdir); + strcat(outFilePath, "/"); + strcat(outFilePath, get_filename(infile)); + + /* Change the file extension */ + char* dotptr = strrchr(outFilePath, '.'); + snprintf(&dotptr[1], strlen(dotptr), "rmh"); + + // if(doesFileExist(outFilePath)) + // { + // printf("Output for %s already exists\n", infile); + // return; + // } + + /* Read input file */ + FILE* fp = fopen(infile, "rb"); + fseek(fp, 0L, SEEK_END); + long sz = ftell(fp); + fseek(fp, 0L, SEEK_SET); + char rmdInStr[sz + 1]; + fread(rmdInStr, sz, 1, fp); + rmdInStr[sz] = 0; + fclose(fp); + + writeHeatshrinkFile((uint8_t*)rmdInStr, sz, outFilePath); +} \ No newline at end of file diff --git a/tools/spiffs_file_preprocessor/src/rmd_processor.h b/tools/spiffs_file_preprocessor/src/rmd_processor.h new file mode 100644 index 000000000..03b1a82b8 --- /dev/null +++ b/tools/spiffs_file_preprocessor/src/rmd_processor.h @@ -0,0 +1,6 @@ +#ifndef _RMD_PROCESSOR_H_ +#define _RMD_PROCESSOR_H_ + +void process_rmd(const char* infile, const char* outdir); + +#endif \ No newline at end of file diff --git a/tools/spiffs_file_preprocessor/src/spiffs_file_preprocessor.c b/tools/spiffs_file_preprocessor/src/spiffs_file_preprocessor.c index 54d2b27ae..eb0959789 100644 --- a/tools/spiffs_file_preprocessor/src/spiffs_file_preprocessor.c +++ b/tools/spiffs_file_preprocessor/src/spiffs_file_preprocessor.c @@ -16,6 +16,7 @@ #include "bin_processor.h" #include "txt_processor.h" #include "midi_processor.h" +#include "rmd_processor.h" const char* outDirName = NULL; @@ -86,6 +87,10 @@ static int processFile(const char* fpath, const struct stat* st __attribute__((u { process_midi(fpath, outDirName); } + else if (endsWith(fpath, ".rmd")) + { + process_rmd(fpath, outDirName); + } break; } case FTW_D: // directory