diff --git a/.gitignore b/.gitignore index 5cc53cd..1807892 100644 --- a/.gitignore +++ b/.gitignore @@ -11,4 +11,7 @@ data/local.yml *.rej *.bak *.kate-swp - +.env +__pycache__/ +*.py[cod] +data/*.json diff --git a/.gitmodules b/.gitmodules index 01d5bed..957635d 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,7 @@ [submodule "bits"] path = bits url = https://github.com/redeclipse/www-bits.git + +[submodule "maps"] + path = maps + url = https://github.com/redeclipse/maps.git diff --git a/404.md b/404.md index 43ae8c4..3ad6e70 100644 --- a/404.md +++ b/404.md @@ -8,4 +8,4 @@ The page you were looking for could not be found. Please check the URL and try a If you continue to have problems, try starting from the **[Home Page](/)**. -You can get further help from our **[Documentation](/docs/Home)**, **[Live Chat](/chat)**, or **[Discussion Area](/discuss)**. +You can get further help from our **[Documentation](/docs/Home)**, **[Discord](/discord)**, or **[Discussion Area](/discuss)**. diff --git a/CNAME b/CNAME index 26810de..7888e1a 100644 --- a/CNAME +++ b/CNAME @@ -1 +1 @@ -www.redeclipse.net \ No newline at end of file +redeclipse.net diff --git a/Readme.md b/Readme.md index 187ef7e..4fa2c65 100644 --- a/Readme.md +++ b/Readme.md @@ -1,6 +1,6 @@ -Welcome to the Red Eclipse Website repository. This is a work in progress to update the website in preparation for v2.0. +Welcome to the Red Eclipse Website repository. This is a work in progress to update the website in preparation for v2.1. -This repository is automatically deployed by [GitHub Pages](https://pages.github.com/) to our [Website](https://www.redeclipse.net/) and includes pages built from the [Documentation Repository](https://github.com/redeclipse/docs). +This repository is automatically deployed by [GitHub Pages](https://pages.github.com/) to our [Website](https://redeclipse.net/) and includes pages built from the [Documentation Repository](https://github.com/redeclipse/docs). When editing pages here, there are a few guidelines you should follow. It is also important to understand how the pre-processors work, like [Jekyll](https://jekyllrb.com/docs/home/) and [Kramdown](https://kramdown.gettalong.org/documentation.html). The configuration for everything is held in [_config.yml](https://github.com/redeclipse/redeclipse.github.io/blob/master/_config.yml). @@ -76,3 +76,86 @@ All paths need to be relative to each other. The docs repository is converted to ### Table of Contents Kramdown supports the automatic generation of these, and the pages are automatically generated to include them. You do **not** need to create your own at the top of your pages. + +# Server browser + +The system operates in a multi-stage process to minimize the load on the Master server while ensuring maximum data accuracy. + + +### 1. Python Poller (Backend) +The **Python Poller** acts as the heart of the system: +* **Master Discovery:** It retrieves the list of all active server IPs from the Red Eclipse Master Server. +* **UDP Queries:** The data is retrieved via **UDP** to get live stats and data. +* **JSON Output:** Collected data is saved into structured **JSON files**. This decouples data collection from the display, ensuring the website stays fast. + +### 2. Server browser (Frontend) +The **Frontend** serves as the user interface for the community: +* The Server browser on the homepage parses the generated JSON files. +* Data is rendered into a user-friendly, responsive layout. +* Players can see exactly where the action is without needing to open the game client. + +### 3. Notifications +Beyond just displaying data, the system includes a **Threshold-based Notification Mode**: +* **Threshold Check:** The poller monitors player counts against a set limit (e.g., "Notify when 4+ players are online"). +* **Trigger:** Once the player count hits the threshold, the system triggers a notification (Browser notification or Discord Webhook). + +--- + +## System Benefits + +* **Efficiency:** UDP pings and JSON caching keep web performance high, regardless of game server response times. +* **Automation:** Players don't have to manually refresh; the system "calls" the community when a player joins. +* **Mobile Ready:** The frontend is optimized so that server lists and stats remain perfectly readable on smartphones. + +--- + + +# Running the Homepage (Jekyll) + +The website is built using **Jekyll**, a Ruby-based static site generator. Ensure **Ruby** and **RubyGems** are installed on your system. + +## Installing + +Clone the git repository and navigate to the top-level directory. Install the dependencies using Bundler: + + bundle install + +*Note: If you don't have Bundler, install it first with `gem install bundler`.* + +## Running + +To launch the local development server, run: + + bundle exec jekyll serve --watch + +By default, the web UI will be available at `http://localhost:4000`. Jekyll will automatically watch for file changes and regenerate the site. + +--- + +# Running the Server Browser (Python poller) + +The data poller is a **Python** application. Ensure **Python 3.8+** is installed. + +## Installing + +Clone the repository containing the poller and install the necessary Python packages: + + pip install -r requirements.txt + +## Running + +The poller communicates with the master server via UDP and generates the JSON data used by the frontend. Run it with: + + python3 getservers.py + +### Advanced Usage + +To enable the legacy notification mode (watching for player thresholds) and debug output: + + python3 getservers.py [-h] [-d] + + options: + -h, --help show this help message and exit + -d, --debug + +By default, the script writes its JSON output to the directory used by the homepage. Edit the configuration variables at the top of `getservers.py` or provide command-line arguments to change paths and notification thresholds. diff --git a/_config.yml b/_config.yml index 28e4d63..9c6d3b6 100755 --- a/_config.yml +++ b/_config.yml @@ -23,12 +23,16 @@ exclude: - ".github" - "plugins" - "layouts" - - "data" - "includes" - "_config.yml" - "CNAME" - "Gemfile" - "Gemfile.lock" + - ".env" + +keep_files: + - "notify.json" + encoding: "utf-8" markdown_ext: "markdown,mkdown,mkdn,mkd,md" strict_front_matter: false diff --git a/bits b/bits deleted file mode 160000 index fd19b68..0000000 --- a/bits +++ /dev/null @@ -1 +0,0 @@ -Subproject commit fd19b68454a4d00069118c9a27c1a60dae2a56e4 diff --git a/data/extlinks.yml b/data/extlinks.yml index 513c4e8..a47ad65 100644 --- a/data/extlinks.yml +++ b/data/extlinks.yml @@ -1,12 +1,3 @@ -- url: /github - title: "GitHub" - class: "fab fa-github" -- url: /discord - title: "Discord" - class: "fab fa-discord" -- url: /steam - title: "Steam" - class: "fab fa-steam" - url: /reddit title: "Reddit" class: "fab fa-reddit" diff --git a/data/game.yml b/data/game.yml index 9505887..dac9948 100644 --- a/data/game.yml +++ b/data/game.yml @@ -1,8 +1,11 @@ -version: "2.0.0" -release: "Jupiter Edition" -date: "19th December 2019" +version: "2.0.9" +release: "Big Bang Beta" +date: "24th December 2025" +#version: "2.1.0" +#release: "Big Bang Edition" +#date: "24th December 2026" youtube: p7w4LXJ_JJ8 -copyright: "2009-2020 Quinton Reeves, Lee Salzman" +copyright: "Quinton Reeves, Lee Salzman" screenshots: pages: 20 items: 4 diff --git a/data/navigation.yml b/data/navigation.yml index 9b903b3..e2be923 100755 --- a/data/navigation.yml +++ b/data/navigation.yml @@ -1,15 +1,24 @@ +- url: /steam + title: "Steam" + class: "fab fa-steam" +- url: /discord + title: "Discord" + class: "fab fa-discord" +- url: /servers + title: "Server browser" + class: "fas fa-server" - url: /download title: "Downloads" class: "fas fa-download" - url: /docs title: "Documentation" class: "fas fa-book" +- url: /github + title: "GitHub" + class: "fab fa-github" - url: /discuss title: "Discussions" class: "fas fa-comment" - url: /issues title: "Issue Tracker" class: "fas fa-tasks" -- url: /servers - title: "Server List" - class: "fas fa-server" diff --git a/docs/FAQ.md b/docs/FAQ.md index 312ba78..898ed4d 100644 --- a/docs/FAQ.md +++ b/docs/FAQ.md @@ -95,11 +95,11 @@ On Windows, when Red Eclipse detects the renderer as 'GDI Generic' it will repor ## Can I contribute to the game? -Development of Red Eclipse is open and community driven. Contributions can be maps created with the in-game ~~[editor](Editing-Basics)~~, art assets like models, or modifications of the [source code](https://github.com/redeclipse/base/tree/master/src). For further information, please read our [contribution guidelines](Contributing). +Development of Red Eclipse is open and community driven. Contributions can be maps created with the in-game [editor](editing/Basics), art assets like models, or modifications of the [source code](https://github.com/redeclipse/base/tree/master/src). For further information, please read our [contribution guidelines](Contributing). ## I found a cheater, what can I do? -If you think someone cheats or violates the [Multiplayer Guidelines](https://raw.githubusercontent.com/redeclipse/base/master/doc/guidelines.txt) in some other way, you can open a issue in the [discussion area](/discuss). Be sure to attach a ~~[demo](Demo-Guide)~~ record of the game, so the problem can be investigated. Convictable cheaters will be sanctioned accordingly. +If you think someone cheats or violates the [Multiplayer Guidelines](https://raw.githubusercontent.com/redeclipse/base/master/doc/guidelines.txt) in some other way, you can open a issue in the [discussion area](/discuss). Be sure to attach a demo record of the game, so the problem can be investigated. Convictable cheaters will be sanctioned accordingly. ## How can i change the chat colour? @@ -109,11 +109,11 @@ Only use bright colours with good contrast. A value of -1 will take your profile ## What is the game objective? -This depends on the current ~~[mode and mutators](Modes-and-Mutators)~~ of the game, which you can look up any time in the help menu (default key: **F1**). Click the large icons in the help menu to learn more about the rules of the current game. These help menus provide detailed information and tips, of which most can also be read on the wiki. There is also a ~~[guide](Gameplay-Guide)~~ for beginners. +This depends on the current [mode and mutators](gameplay/Gameplay-Guide) of the game, which you can look up any time in the help menu (default key: **F1**). Click the large icons in the help menu to learn more about the rules of the current game. These help menus provide detailed information and tips, of which most can also be read on the wiki. There is also a [guide](gameplay/Gameplay-Guide) for beginners. ## Why is my player score negative? -Be careful not to shoot your team mates, especially when using explosive weapons. Each team kill subtracts six points. Team kills on ~~[flag carriers](Capture-the-Flag)~~ or ~~[bombers](Bomber-ball)~~ even double this penalty. For details, see [Deathmatch Scoring](Deathmatch#scoring). +Be careful not to shoot your team mates, especially when using explosive weapons. Each team kill subtracts six points. Team kills on [flag carriers](gameplay/Capture-the-Flag) or Bomber-ball even double this penalty. For details, see [Deathmatch Scoring](gameplay/Gameplay-Guide). ## dm, pzap, gg - what did they just say? @@ -121,7 +121,7 @@ For frequently used abbreviations and player slang, see [glossary](Glossary). ## What are those symbols near player names? -These are the ~~[privileges](Privileges)~~ or ranks of registered players. You can request your own player account [here](/apply). +These are the Privileges or ranks of registered players. ## Why is there a timer when I get killed? @@ -133,15 +133,15 @@ It is easy to hit someone with a shotgun, but the weapon is really only effectiv ## How did they take away their own flag? -In ~~[capture](Capture-the-Flag)~~ games, you can press **F** (per default) to pick up the flag in your own base, so the enemy cannot reach it that easily. The same button can also be used to drop a flag you carry, be it yours or the enemy's. +In [capture](gameplay/Capture-the-Flag) games, you can press **F** (per default) to pick up the flag in your own base, so the enemy cannot reach it that easily. The same button can also be used to drop a flag you carry, be it yours or the enemy's. ## Why are there no health pickups? -In Red Eclipse the player regenerates health and ~~[impulse](Parkour-Guide)~~ energy. Therefore, map control is less important, and everyone can focus on the real fun: Capturing that flag, grabbing that bomber ball… or just fragging! +In Red Eclipse the player regenerates health and [impulse](gameplay/Parkour-Guide) energy. Therefore, map control is less important, and everyone can focus on the real fun: Capturing that flag, grabbing that bomber ball… or just fragging! ## Why am I taking damage when no one is near me? -Your are under the effect of the a negative ~~[status effect](Status-Effects)~~. +Your are under the effect of the a negative Status-Effects. ## A green beeping thing obscured my vision - what the heck? diff --git a/docs/Glossary.md b/docs/Glossary.md index 9d3c087..26d2b1b 100644 --- a/docs/Glossary.md +++ b/docs/Glossary.md @@ -14,7 +14,7 @@ The following list explains some commonly used terms and acronyms. | term | Description | |--------|-------------| | afk | away from keyboard. When spectators go AFK, they can and should alter their /name accordingly. Going AFK on a nearly full server can result in a kick. | -| auth | authentication, a key for registered players to identify. Often used as synonym for player accounts or ~~[privileges](Privileges)~~. | +| auth | authentication, a key for registered players to identify. Often used as synonym for player accounts or Privileges. | | bb | bomber-ball, a game mode. Or bye bye. | | bot | usually refers to an A.I. controlled player. Not to confuse with aim-bot, a client side modification that is considered cheating according to the Multiplayer Guidelines. | | cc | creative-commons license, typically used for licensing of contributions. | @@ -30,12 +30,12 @@ The following list explains some commonly used terms and acronyms. | gaud | the official unit for gaudiness. 100° C is hot enough to vaporize your eyeballs; likewise 100 gaud is gaudy enough to do the same. For an example of a 100-gaud map, see Map Rooftop | | insta | instagib, a game mutator for one-hit-kills, disabled by default. | | mat | materials define special properties of map volumes, such as filling an area with water or clipping. | -| mod | moderator, a player with elevated ~~[privileges](Privileges)~~. Mod can also stand for client modifications. | +| mod | moderator, a player with elevated Privileges. Mod can also stand for client modifications. | | mpz | file format to store most content of a map, including all geometry, entity positions and lightmaps. | | muts | game mutators, special game rules that can be added and combined for a more diverse game experience. | | nade | grenade, a collectible explosive weapon. | | ogg | container format for vorbis audio, used for music and sound files. | -| op | operator, a player with advanced ~~[privileges](Privileges)~~. Or just OverPowered. | +| op | operator, a player with advanced Privileges. Or just OverPowered. | | pzap | a typical taunt or chat comment, echoism of a rifle shot. | | runner | players with a preference for parkour and agile game-play, like race games or hit-and-run tactics in capture-the-flag or bomber-ball games. | | sg | shotgun, a short ranged loadout weapon. | diff --git a/docs/Home.md b/docs/Home.md index 50648ac..2e773f6 100755 --- a/docs/Home.md +++ b/docs/Home.md @@ -20,7 +20,6 @@ redirect_from: - **[Gameplay Guide](gameplay/Gameplay-Guide)** - Want to learn how to play? This is a good start. - **[Learn about the Weapons](gameplay/Weapons-Guide)** - Each weapon is unique, learn the pros and cons. - **[Tips and Tricks for using Parkour](gameplay/Parkour-Guide)** - Everyone loves running on walls, but there's more to learn. -- **[Official Maps](Official-Maps)** - Descriptions and recommendations for the included levels. ## Server Administration - **[Server Setup](server-how-to/Server-Setup)** - How to configure and run a Red Eclipse server. @@ -29,10 +28,8 @@ redirect_from: ## Map Editing - **[Editing Basics](editing/Basics)** - Learn how to start making your own maps in Red Eclipse. -- **~~[Editing Reference](editing/Reference)~~** - A detailed explanation of the available editor features. ## Development -- **[Version 2.0](Information-for-v2)** - Find out more about the upcoming **v2.0**, featuring [Tesseract](http://tesseract.gg/). - **[Contributing](Contributing)** - Instructions on contributing to the Red Eclipse project. - **[Core Principles](Core-Principles)** - Some of the ideas that shape how Red Eclipse as a game is designed. - **[Debugging](Debug)** - How to gather information for debugging. @@ -44,12 +41,9 @@ redirect_from: - **[Facebook Messenger](/messenger)** - Ask a quick question and we'll get back to you. - **[Issue Tracker](/issues)** - The place for bug reports and approved feature requests. - **[Contribution Guidelines](/contribute)** - See our rules and suggestions for contributing. -- **[Game Statistics](/stats)** - The place to see statistics collected by our master server. -- **[Server List](/servers)** - See a nice list of server and who's playing on them. - **[Official SubReddit](/reddit)** - The place to share with your fellow Reddit'ors. -- **[GameJolt Page](/gamejolt)** - Leave us a review or use the GameJolt client to manage your game. - **[Facebook Page](/facebook)** - Get social with our Facebook page and share your review of the game. - **[YouTube Page](/youtube)** - The place to see Red Eclipse related videos and live streams. - **[GitHub Project](/github)** - We do all our development on GitHub, so this is the place to start when developing. -- **[Subscribe via Patreon](/patreon)** - Pledge to our Patreon in order to help cover project costs. +- **[Sponsor via Open Collective](/opencollective)** - Sponsor the project and help cover project costs. - **[Donate via PayPal](/paypal)** - Make a one-time or recurring donation to help cover project costs. diff --git a/docs/Install-Guide.md b/docs/Install-Guide.md index 94c41b8..0af4b50 100644 --- a/docs/Install-Guide.md +++ b/docs/Install-Guide.md @@ -43,14 +43,11 @@ From the command line: - Enter the source directory with `cd src` - Run the game with `./redeclipse.sh` -## macOS -- [Download the macOS tarball](/download/macos). -- When it is done, click the resulting TAR.BZ2 file in your downloads folder on the dock. -- Archive utility will extract the contents into the same folder and open a Finder window with redeclipse.app highlighted. -- Drag and drop the redeclipse.app package to your favourite location (Desktop, Applications folder, whatever). -- Run redeclipse.app. - -If this is the first time running the app, the operating system complains that it can't run an unsigned package, simply right (or cmd) click and select Open. In future you will be able to run the app as normal, this will override the warning for all future attempts. You can also do this from the command line. - ## If you get stuck Don't panic! If you have trouble working out how to install and run the game, you can get assistance on our [Discord](/discord) or [Discussions](/discuss). Please be ready to provide as much information as possible, especially what operating system you're on and specifically which package you're trying to install! + +## Important Notice About Unofficial Packages +Red Eclipse is available only through the official installation methods described on this page or via **[Steam](/steam)**. +Packages found on Flathub, Snap, Flatpak, AUR, Linux distribution repositories, the Ubuntu Software Store, or any other third-party source are not official, not maintained, and not supported by the Red Eclipse development team.h +These unofficial packages may be outdated, incomplete, or insecure, and can cause issues that we cannot assist with. +For the best experience and to ensure you're receiving verified, up-to-date builds, please install Red Eclipse exclusively through the official methods provided here or via **[Steam](/steam)**. diff --git a/docs/License.md b/docs/License.md index 14aa278..ea64d6d 100644 --- a/docs/License.md +++ b/docs/License.md @@ -12,9 +12,9 @@ redirect_from: ## THE RED ECLIPSE LICENSE Red Eclipse is based on Tesseract and Cube Engine 2, both of which are covered under the ZLIB license, you may use the source code so long as you obey this license. -> Red Eclipse, Copyright (C) 2009-2019 Quinton Reeves, Lee Salzman -> Tesseract, http://tesseract.gg/ Copyright (C) 2014-2019 Wouter van Oortmerssen, Lee Salzman, Mike Dysart, Robert Pointon, Quinton Reeves, and Benjamin Segovia -> Cube Engine 2, http://cubeengine.com/ Copyright (C) 2001-2019 Wouter van Oortmerssen, Lee Salzman, Mike Dysart, Robert Pointon, and Quinton Reeves +> Red Eclipse, Copyright (C) 2009–{{ site.time | date: "%Y" }} Quinton Reeves, Lee Salzman +> Tesseract, http://tesseract.gg/ Copyright (C) 2014–{{ site.time | date: "%Y" }} Wouter van Oortmerssen, Lee Salzman, Mike Dysart, Robert Pointon, Quinton Reeves, and Benjamin Segovia +> Cube Engine 2, http://cubeengine.com/ Copyright (C) 2001–{{ site.time | date: "%Y" }} Wouter van Oortmerssen, Lee Salzman, Mike Dysart, Robert Pointon, and Quinton Reeves > http://www.opensource.org/licenses/zlib-license.php This software is provided 'as-is', without any express or implied warranty. In no event will the authors be held liable for any damages arising from the use of this software. Permission is granted to anyone to use this software for any purpose, including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: @@ -27,7 +27,7 @@ The license covers the source code, shells scripts, and related config files. Th Content included in the game (maps, textures, sounds, models etc.) is NOT covered by this license, and may have individual copyrights and distribution restrictions (see individual readmes), note that all content in Red Eclipse is intended to be "open source" friendly and is usually some variation of the Creative Commons license *excluding* any non-commercial licenses. Red Eclipse does not accept content which has usage restrictions beyond "BY" (By, give credit) and "SA" (Share-alike, share derivative works). In the absence of an explicit license, content is considered to be covered by the CC-BY-SA license, either version 4.0 or (at your option) any later version, and you may use the content in Red Eclipse so long as you obey individual licensing criteria. -> Red Eclipse, Copyright (C) 2009-2019 Red Eclipse Team +> Red Eclipse, Copyright (C) 2009–{{ site.time | date: "%Y" }} Red Eclipse Team > Creative Commons Attribution ShareAlike 4.0+ License (CC-BY-SA) > See cc-by-sa.txt or http://creativecommons.org/licenses/by-sa/4.0/ diff --git a/docs/Trademark-Policy.md b/docs/Trademark-Policy.md index 1b4d381..6b76c9d 100644 --- a/docs/Trademark-Policy.md +++ b/docs/Trademark-Policy.md @@ -160,7 +160,7 @@ as described above. ## Attribution -This text is Copyright (C) 2011-2019, the Red Eclipse Team +This text is Copyright (C) 2011-2025, the Red Eclipse Team and is available under a Creative Commons Attribution-ShareAlike 4.0 License diff --git a/docs/editing/Models.md b/docs/editing/Models.md index 6d90c6e..425ddd8 100644 --- a/docs/editing/Models.md +++ b/docs/editing/Models.md @@ -51,7 +51,7 @@ Within blender: save your mesh.obj file to the same folder as your obj.cfg -A correctly configured example of a model can be found here; Download this [map](https://github.com/redeclipse/docs/raw/master/editing/monkey-map.rar) and [model](https://github.com/redeclipse/docs/raw/master/editing/monkey-model.rar). Extract the two folders to the root directory of your [local data folder](https://www.redeclipse.net/docs/FAQ#where-do-i-find-screenshots-logs-and-other-user-data). Load the example map in game with by running `map monkey` at the console. You should see Blender's default monkey mesh present within the map. +A correctly configured example of a model can be found here; Download this [map](https://github.com/redeclipse/docs/raw/master/editing/monkey-map.rar) and [model](https://github.com/redeclipse/docs/raw/master/editing/monkey-model.rar). Extract the two folders to the root directory of your [local data folder](https://redeclipse.net/docs/FAQ#where-do-i-find-screenshots-logs-and-other-user-data). Load the example map in game with by running `map monkey` at the console. You should see Blender's default monkey mesh present within the map. #### iqm This example uses a mesh exported for use as an iqm model. diff --git a/docs/gameplay/Gameplay-Guide.md b/docs/gameplay/Gameplay-Guide.md index 4913235..5c14e18 100644 --- a/docs/gameplay/Gameplay-Guide.md +++ b/docs/gameplay/Gameplay-Guide.md @@ -38,7 +38,7 @@ In some gamemodes where teams are present, the server may randomly swap some pla #### Affinities An affinity is an object in game which can be collected to score points, such as a flag or a ball. -In game modes [**Capture the Flag**](Capture-the-Flag) and ~~[**Bomber Ball**](Bomber-ball)~~, you can throw the affinity to your teammates by pressing [F]. +In game modes [**Capture the Flag**](Capture-the-Flag) and **Bomber Ball**, you can throw the affinity to your teammates by pressing [F]. #### Friendly fire @@ -56,7 +56,7 @@ Some maps will have a layout that is not perfectly symmetrical where one team ma | |**Editing** |Create and edit existing maps | | |**Deathmatch** |Shoot to kill and increase score by fragging | | |[**Capture the Flag**](Capture-the-Flag) |Take the enemy flag and return it to the base to score| -| |~~[**Defend and Control**](Bomber-ball)~~|Defend control points to score | +| |**Defend and Control**|Defend control points to score | | |**Bomber Ball** |Carry the bomb into the enemy goal to score | | |**Race** |Compete for the fastest time completing a lap | diff --git a/download/index.md b/download/index.md index 499dc38..434adf1 100755 --- a/download/index.md +++ b/download/index.md @@ -7,22 +7,26 @@ redirect_from: --- # v{{ site.data.game.version }} ({{ site.data.game.release }}) -### Released {{ site.data.game.date }} +

+ + Install with Steam + +

+The best way to play Red Eclipse is by downloading it through **[Steam](/steam)**. It is free of charge, and you will get the latest updates automatically, as well as have Steam features available in-game. -The best way to play Red Eclipse is by downloading it through **Steam**. It is free of charge, and you will get the latest updates automatically, as well as have Steam features available in-game. If you'd rather not use **Steam** (as described above), you can still download a static installable package, but please note that only the Linux AppImage provides automatic updates, any other version will require you to update your installation manually each release. The Red Eclipse team does not provide support for outdated versions of the software. +##### Install without Steam (NOT RECOMMENDED) +If you'd rather not use **[Steam](/steam)** (as described above), you can still download a static installable package, but please note that only the Linux AppImage provides automatic updates, any other version will require you to update your installation manually each release. The Red Eclipse team does not provide support for outdated versions of the software. Platform | Downloads | Other Sources ---------------------------------------------------------------------|-------------------------------------|------------------------------------- - **Steam** | **[Store Page](/steam)** | - **Windows** | **[Installer](/download/win)** | [Torrent](/download/torrent/win) - [ZIP](/download/zip) **Linux/BSD** | **[TAR.BZ2](/download/nix)** | [Torrent](/download/torrent/nix) - **macOS** | **[TAR.BZ2](/download/mac)** | [Torrent](/download/torrent/mac) **Combined** | **[TAR.BZ2](/download/combined)** | [Torrent](/download/torrent/combined) **[Installation Help](/docs/Install-Guide)** ### System Requirements -Red Eclipse 2 requires a fairly modern graphics card to run, but is otherwise quite tolerant of hardware specifications. If you find you can't run the game due to insufficient hardware, you might want to try the [last release of Red Eclipse v1.6](https://github.com/redeclipse/base/releases/tag/v1.6.0) which runs on Cube Engine 2 alone without the modern renderer from Tesseract. +Red Eclipse requires a fairly modern graphics card to run, but is otherwise quite tolerant of hardware specifications. If you find you can't run the game due to insufficient hardware, you might want to try the [old unsupported Version of Red Eclipse v1.6](https://github.com/redeclipse/base/releases/tag/v1.6.0) (Can also be installed via **[Steam](/steam)**) which runs on Cube Engine 2 alone without the modern renderer from Tesseract. #### MINIMUM * Processor: Intel Pentium Dual-Core E2180 / AMD Athlon 64 X2 4200+ @@ -43,3 +47,4 @@ Red Eclipse 2 requires a fairly modern graphics card to run, but is otherwise qu {% include release.md %} You can view the entire release [on GitHub](https://github.com/redeclipse/base/releases/tag/v{{ site.data.game.version }}). + diff --git a/getservers.py b/getservers.py new file mode 100644 index 0000000..de39597 --- /dev/null +++ b/getservers.py @@ -0,0 +1,826 @@ +""" +Red Eclipse server list fetcher and notifier. +- Queries the master server, polls each game server via UDP, and writes JSON outputs. +- Optionally posts Discord webhook notifications when new players join. +""" + +import socket +import shlex +import struct +import time +import json +import re +import urllib.request +import argparse +import os +from collections import namedtuple +from datetime import datetime, timezone +from pathlib import Path +from dotenv import load_dotenv + +# Load environment variables from .env (local use only) +load_dotenv() + +# ========================================== +# CONFIGURATION +# ========================================== + +MASTER_HOST = "play.redeclipse.net" +MASTER_PORT = 28800 + +# Timeouts +TIMEOUT_TCP = 5.0 +TIMEOUT_UDP = 2.0 + +# Discord bot settings +DISCORD_BOT = 1 +DISCORD_BOT_NAME = "Server browser" +DISCORD_WEBHOOK_URL = os.getenv("DISCORD_WEBHOOK_URL") # Discord webhook URL needs to be added as repository secret in GitHub (Load environment variables from .env for local use only) +DISCORD_SERVER_LINK = "https://redeclipse.net/servers/" +DISCORD_PING_ROLE = "" + +# Delays +NETWORK_THROTTLE = 0.05 + +# Output paths +DATA_DIR = Path('data') +OUTPUT_FILE = DATA_DIR / 'servers.json' +TIME_FILE = DATA_DIR / 'time.json' +MAPS_DIR = Path('maps') +MAPS_FILE = DATA_DIR / 'maps.json' +NOTIFY_FILE = 'notify.json' +IP_CACHE_FILE = DATA_DIR / 'ip.json' + +# Global cache for IP lookups +IP_CACHE = {} +# Global args for debug printing +ARGS = None + +# ========================================== +# CONSTANTS +# ========================================== + +MUTATORS = { + 'ffa': 1 << 0, 'coop': 1 << 1, 'instagib': 1 << 2, 'medieval': 1 << 3, + 'kaboom': 1 << 4, 'duel': 1 << 5, 'survivor': 1 << 6, 'classic': 1 << 7, + 'onslaught': 1 << 8, 'vampire': 1 << 9, 'resize': 1 << 10, 'hard': 1 << 11, + 'arena': 1 << 12, 'dark': 1 << 13, 'gsp1': 1 << 14, 'gsp2': 1 << 15, 'gsp3': 1 << 16 +} + +MODE_SPECIFIC_MUTATORS = { + 'deathmatch': ['gladiator', 'oldschool'], + 'capture-the-flag': ['quick', 'defend', 'protect'], + 'defend-and-control': ['quick', 'king'], + 'bomber-ball': ['hold', 'basket', 'assault'], + 'race': ['lapped', 'endurance', 'gauntlet'], + 'speedrun': ['lapped', 'endurance', 'gauntlet'], +} + +GAME_MODES = ['demo', 'editing', 'deathmatch', 'capture-the-flag', 'defend-and-control', 'bomber-ball', 'speedrun'] +MASTER_MODES = ['open', 'veto', 'locked', 'private', 'password'] +PRIVILEGE_NAMES = ['administrator', 'developer', 'founder', 'localadministrator', 'localmoderator', + 'localoperator', 'localsupporter', 'moderator', 'none', 'operator', 'player', 'supporter'] + +# Icon mapping for Discord footer (game modes) +MODE_ICON_MAP = { + 'capture-the-flag': 'capture.png', + 'capture': 'capture.png', + 'defend-and-control': 'defend.png', + 'defend': 'defend.png', + 'bomber-ball': 'bomber.png', + 'bomber': 'bomber.png', + 'editing': 'editing.png', + 'demo': 'demo.png', + 'deathmatch': 'deathmatch.png', + 'race': 'speedrun.png', + 'speedrun': 'speedrun.png' +} + +# Icon mapping for Discord author (master modes) +MASTERMODE_ICONS = { + 'full': 'disconnect.png', + 'open': 'connect.png', + 'veto': 'failed.png', + 'failed': 'failed.png', + 'locked': 'locked.png', + 'private': 'locked.png', + 'password': 'locked.png', + 'unknown': 'unknown.png' +} + +# Cube 2 Protocol Unicode mapping +CUBE2_UNICHARS = [ + 0, 192, 193, 194, 195, 196, 197, 198, 199, 9, 10, 11, 12, 13, 200, 201, 202, 203, 204, 205, 206, 207, 209, 210, + 211, 212, 213, 214, 216, 217, 218, 219, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, + 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, + 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, + 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, + 220, 221, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 241, 242, 243, + 244, 245, 246, 248, 249, 250, 251, 252, 253, 255, 0x104, 0x105, 0x106, 0x107, 0x10C, 0x10D, 0x10E, 0x10F, 0x118, + 0x119, 0x11A, 0x11B, 0x11E, 0x11F, 0x130, 0x131, 0x141, 0x142, 0x143, 0x144, 0x147, 0x148, 0x150, 0x151, 0x152, + 0x153, 0x158, 0x159, 0x15A, 0x15B, 0x15E, 0x15F, 0x160, 0x161, 0x164, 0x165, 0x16E, 0x16F, 0x170, 0x171, 0x178, + 0x179, 0x17A, 0x17B, 0x17C, 0x17D, 0x17E, 0x404, 0x411, 0x413, 0x414, 0x416, 0x417, 0x418, 0x419, 0x41B, 0x41F, + 0x423, 0x424, 0x426, 0x427, 0x428, 0x429, 0x42A, 0x42B, 0x42C, 0x42D, 0x42E, 0x42F, 0x431, 0x432, 0x433, 0x434, + 0x436, 0x437, 0x438, 0x439, 0x43A, 0x43B, 0x43C, 0x43D, 0x43F, 0x442, 0x444, 0x446, 0x447, 0x448, 0x449, 0x44A, + 0x44B, 0x44C, 0x44D, 0x44E, 0x44F, 0x454, 0x490, 0x491 +] + +# ========================================== +# DATA STRUCTURES +# ========================================== + +Server = namedtuple('Server', ['ip', 'port', 'name', 'branch']) +Player = namedtuple('Player', ['inputpos', 'name', 'raw_name', 'privilege']) +Status = namedtuple('Status', ['clients', 'max_clients', 'map_name', 'game_mode', 'master_mode', + 'time_left', 'mutators', 'version', 'branch', 'major_version', + 'minor_version', 'patch_version', 'players', 'raw_response']) + +COLOR_PATTERN = re.compile(r'\f[a-zA-Z0-9\[\]]') +PRIV_PATTERN = re.compile(r'\$priv([a-z]+)tex') + +# ========================================== +# HELPER FUNCTIONS +# ========================================== + +def run_discord_debug_check(player_name, server_data): + if not DISCORD_WEBHOOK_URL: + return "[DEBUG-Discord-weebhook] FAILED: Webhook URL is not configured in .env" + + # Clamp player name to max 20 chars and adjust server name length to keep title width stable + player_display_name = str(player_name) if player_name is not None else "" + if len(player_display_name) > 20: + player_display_name = player_display_name[:20] + server_max_len = 23 + (20 - len(player_display_name)) + server_name = truncate_display_name(server_data.get('name', 'Unknown'), server_max_len) + content = f"(Debug Connection Check)" + + payload = {"content": content, "username": DISCORD_BOT_NAME} + + try: + req = urllib.request.Request( + DISCORD_WEBHOOK_URL, + data=json.dumps(payload).encode('utf-8'), + headers={'Content-Type': 'application/json', 'User-Agent': 'RedEclipseBot/1.0'} + ) + with urllib.request.urlopen(req, timeout=5.0) as response: + return f"[DEBUG-Discord-weebhook] Success! Server responded with: {response.status}" + except Exception as e: + return f"[DEBUG-Discord-weebhook] FAILED: {e}" + +def send_discord_webhook(trigger_player_name, server_data): + if not DISCORD_WEBHOOK_URL: + if ARGS and ARGS.debug: print("[DEBUG-Discord-weebhook] Webhook URL missing.") + return + + # Clamp player name to max 20 chars and adjust server name length accordingly + player_display_name = str(trigger_player_name) if trigger_player_name is not None else "" + if len(player_display_name) > 20: + player_display_name = player_display_name[:20] + server_max_len = 23 + (20 - len(player_display_name)) + server_name = truncate_display_name(server_data.get('name', 'Unknown'), server_max_len) + + location = server_data.get('location', 'Unknown') + map_name = server_data.get('map', 'Unknown') + game_mode = server_data.get('gamemode', 'UNKNOWN') + + # Format game mode for display using Liquid-like rules + minor_words_set = {"a", "an", "the", "and", "but", "or", "for", "nor", "so", "yet", "as", "at", "by", "in", "of", "off", "on", "per", "to", "up", "with"} + def _format_mode_titlecase(mode_str): + try: + words = mode_str.replace('-', ' ').strip().lower().split() + except Exception: + words = [] + final_words = [] + for idx, w in enumerate(words, start=1): + current = w.strip() + if not current: + continue + capitalize_word = True + if idx > 1 and current in minor_words_set: + capitalize_word = False + final_words.append(current.capitalize() if capitalize_word else current) + return " ".join(final_words) if final_words else "Unknown" + game_mode_display = _format_mode_titlecase(game_mode) + def _capitalize_first(s): + return s[:1].upper() + s[1:] if isinstance(s, str) and s else "" + map_name_display = _capitalize_first(map_name) + + # Stats + active_p = server_data.get('players', 0) + max_p = server_data.get('max_players', 0) + master_mode_raw = server_data.get('mastermode', 'unknown').lower() + master_mode_disp = _capitalize_first(master_mode_raw) + version = server_data.get('version_full', 'N/A') + + # Determine Master Mode Icon (Author Icon) + if max_p > 0 and active_p >= max_p: + mm_icon_file = MASTERMODE_ICONS.get('full', 'disconnect.png') + elif master_mode_raw == 'veto': + mm_icon_file = MASTERMODE_ICONS.get('veto', 'failed.png') + else: + mm_icon_file = MASTERMODE_ICONS.get(master_mode_raw, 'unknown.png') + mastermode_icon_url = f"https://raw.githubusercontent.com/redeclipse/textures/master/servers/{mm_icon_file}" + + # Determine Game Mode Icon (Footer Icon) + mode_lower = game_mode.lower() + if mode_lower in MODE_ICON_MAP: + gamemode_icon_file = MODE_ICON_MAP[mode_lower] + gamemode_icon_url = f"https://raw.githubusercontent.com/redeclipse/textures/master/modes/{gamemode_icon_file}" + else: + gamemode_icon_url = "https://raw.githubusercontent.com/redeclipse/textures/master/servers/unknown.png" + + # Player List + all_players = server_data.get('player_list_data', []) + player_names = [p.get('name', 'Unknown') for p in all_players] + player_names_str = ", ".join(player_names) + if not player_names_str: + player_names_str = "None" + + # Dynamic Timestamp + current_utc_iso = datetime.now(timezone.utc).strftime('%H:%M') + + # Image Logic + image_url = "https://raw.githubusercontent.com/redeclipse/www-bits/master/bg1.jpg" + try: + if MAPS_DIR.exists() and map_name and map_name not in ("Offline/Unknown", "Unknown"): + map_file = MAPS_DIR / f"{map_name.lower()}.png" + if map_file.exists(): + image_url = f"https://raw.githubusercontent.com/redeclipse/maps/refs/heads/master/{map_name.lower()}.png" + except Exception: + pass + + # Build Payload + payload = { + "username": DISCORD_BOT_NAME, + "avatar_url": "https://raw.githubusercontent.com/redeclipse/promotional/master/assets/emblem.png", + "content": DISCORD_PING_ROLE if DISCORD_PING_ROLE else "", + "embeds": [ + { + "title": f"{player_display_name} joined {server_name}", + "url": DISCORD_SERVER_LINK, + "color": 9109504, + "description": f"> **Players** {player_names_str}", + "thumbnail": { + "url": image_url + }, + "author": { + "name": f"{game_mode_display} on {map_name_display}", + "url": DISCORD_SERVER_LINK, + "icon_url": gamemode_icon_url + }, + "footer": { + "text": f"{master_mode_disp} • {active_p}/{max_p} • {version} • {location} • {current_utc_iso} UTC", + "icon_url": mastermode_icon_url + } + } + ] + } + + try: + req = urllib.request.Request( + DISCORD_WEBHOOK_URL, + data=json.dumps(payload).encode('utf-8'), + headers={'Content-Type': 'application/json', 'User-Agent': 'RedEclipseBot/1.0'} + ) + with urllib.request.urlopen(req, timeout=5.0) as r: + if ARGS and ARGS.debug: + print(f"[DEBUG-Discord-weebhook] Embed notification sent for {trigger_player_name}") + except Exception as e: + if ARGS and ARGS.debug: + print(f"[DEBUG-Discord-weebhook] Failed to send webhook embed: {e}") + +def hexdump(data): + lines = [] + for i in range(0, len(data), 16): + chunk = data[i:i+16] + hex_p = " ".join(f"{b:02x}" for b in chunk) + ascii_p = "".join(chr(b) if 32 <= b <= 126 else "." for b in chunk) + lines.append(f"{i:04x} {hex_p:<48} |{ascii_p}|") + return "\n".join(lines) + +def hex_to_rgb(hex_color): + hex_color = hex_color.lstrip('#') + return tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4)) + +def rgb_to_hsl(r, g, b): + r, g, b = r/255.0, g/255.0, b/255.0 + mx, mn = max(r, g, b), min(r, g, b) + df = mx - mn + h = s = 0.0 + l = (mx + mn) / 2.0 + if df != 0: + s = df / (2.0 * l if l <= 0.5 else 2.0 - 2.0 * l) + if mx == r: h = (g - b) / df + (6.0 if g < b else 0.0) + elif mx == g: h = (b - r) / df + 2.0 + else: h = (r - g) / df + 4.0 + h /= 6.0 + return h, s, l + +def generate_css_filter(hex_color): + # Lookup table for exact Red Eclipse team colors + # These filters assume the source icon is black + PRESETS = { + '#808080': "invert(58%) sepia(6%) saturate(14%) hue-rotate(320deg) brightness(89%) contrast(85%)", # Neutral Grey + '#f03030': "invert(24%) sepia(64%) saturate(3509%) hue-rotate(346deg) brightness(94%) contrast(105%)", # Omega Red + '#3030f0': "invert(13%) sepia(91%) saturate(5887%) hue-rotate(244deg) brightness(88%) contrast(106%)", # Alpha Blue + '#000000': "none", + '#ffffff': "invert(100%)" + } + lower_hex = hex_color.lower() + if lower_hex in PRESETS: + return PRESETS[lower_hex] + + # Fallback for unknown custom colors (Approximation) + try: + r, g, b = hex_to_rgb(hex_color) + h, s, l = rgb_to_hsl(r, g, b) + if s < 0.05: # Grayscale fallback + return f"invert(0%) sepia(0%) saturate(0%) brightness({int(l*100)}%) contrast(100%) invert(100%)" + + # Generic heuristic (Rough approximation) + hu = (h * 360) + return f"invert(50%) sepia(100%) saturate(1000%) hue-rotate({hu:.0f}deg) brightness({int(l*100)}%)" + except Exception: + return "none" + +def uncolor_string(s): + return COLOR_PATTERN.sub('', s) if s else "" + +def truncate_display_name(name, max_len=23): + try: + s = str(name) if name is not None else "" + if len(s) > max_len: + return s[:max_len-3] + "..." + return s + except Exception: + return "Unknown" + +def strip_player_data(name): + if "$priv" in name: name = re.sub(r'\(.*\$priv.*\)', '', name) + lb = name.rfind(']') + if lb != -1: name = name[lb+1:] + return uncolor_string(name).strip() + +def extract_colors(raw_name): + matches = re.findall(r'\[(\d+)\]', raw_name) + p_int = int(matches[0]) & 0xFFFFFF if len(matches) > 0 else 16777215 + t_int = int(matches[1]) & 0xFFFFFF if len(matches) > 1 else 16777215 + return p_int, f"#{p_int:06x}", t_int, f"#{t_int:06x}" + +def get_mutator_names(flags, game_mode): + # 1. Get standard mutators up to Arena (Bit 0 to 12) + muts = [m for m, mask in MUTATORS.items() if mask <= (1 << 12) and flags & mask] + + specs = MODE_SPECIFIC_MUTATORS.get(game_mode, []) + + # 2. Check Bit 13: Always "dark" + if flags & (1 << 13): + muts.append('dark') + + # 3. Check Bits 14, 15, and 16: Mode-Specific Mutators + # Bit 14 -> specs[0] + if flags & (1 << 14): + if len(specs) > 0 and specs[0]: + muts.append(specs[0]) + else: + muts.append('gsp1') + # Bit 15 -> specs[1] + if flags & (1 << 15): + if len(specs) > 1 and specs[1]: + muts.append(specs[1]) + else: + muts.append('gsp2') + # Bit 16 -> specs[2] + if flags & (1 << 16): + if len(specs) > 2 and specs[2]: + muts.append(specs[2]) + else: + muts.append('gsp3') + return [m for m in muts if m] + +def ip_to_country(ip): + if ip in IP_CACHE: + return IP_CACHE[ip] + try: + # Sleep for 1.5 seconds only when querying new IPs to respect the 45 req/min limit + time.sleep(1.5) + with urllib.request.urlopen(f"http://ip-api.com/json/{ip}?fields=country", timeout=3.0) as r: + data = json.load(r) + country = data.get('country', 'Unknown') + if country != 'Unknown': + IP_CACHE[ip] = country + return country + except Exception as e: + if ARGS and ARGS.debug: + print(f"[Warning] Geo-IP lookup failed for {ip}: {e}") + return 'Unknown' + +# ========================================== +# PROTOCOL HANDLING +# ========================================== + +class ProtocolStream: + def __init__(self, data, debug=False): + self.data = data + self.offset = 0 + self.debug = debug + + if debug: + print(f"\n[DEBUG-Poller] Parsing UDP Response ({len(data)} bytes)") + + self.read_int("UDP Header") + self.offset = 5 + + def read_int(self, label="int"): + start = self.offset + try: + ch1 = struct.unpack(' 255: + if debug: print(f"[WARNING] Invalid client count {clients} from {ip}") + return None + int_count = stream.read_int("Int Count") + version = stream.read_int("Protocol") + g_mode_c = stream.read_int("Game Mode") + muts = stream.read_int("Mutators") + tl = stream.read_int("Time Left") + mc = stream.read_int("Max Clients") + mm_c = stream.read_int("Master Mode") + stream.read_int("Vars") + stream.read_int("Mods") + + # --- Parse extras --- + int_count -= 8 + maj, minr, pat = 0, 0, 0 + if version >= 226: + maj, minr, pat = stream.read_int("Maj"), stream.read_int("Min"), stream.read_int("Pat") + int_count -= 3 + for _ in range(int_count): stream.read_int("Extra") + + # --- Parse strings --- + m_name = stream.read_string("Map") + _ = stream.read_string("Server Name") + br = stream.read_string("Branch") if version >= 227 else "" + + # --- Parse players --- + p_names = [stream.read_string(f"P{i}") for i in range(clients)] + p_pos = [stream.read_int(f"P{i} Pos") for i in range(clients)] + if version >= 226: + for _ in range(clients): stream.read_string("Skip Args") + + players = [] + for i in range(clients): + raw = p_names[i] + priv = "none" + match = PRIV_PATTERN.search(raw.lower()) + if match: priv = match.group(1) if match.group(1) in PRIVILEGE_NAMES else "none" + players.append(Player(p_pos[i], strip_player_data(raw), raw, priv)) + + if debug: + print("\n--- HEX DUMP ---") + print(hexdump(data)) + print("-" * 50) + + return Status(clients, mc, uncolor_string(m_name), + GAME_MODES[g_mode_c] if 0 <= g_mode_c < len(GAME_MODES) else "unknown", + MASTER_MODES[mm_c] if 0 <= mm_c < len(MASTER_MODES) else "unknown", + tl, muts, version, br, maj, minr, pat, players, data) + except Exception: + return None + +def process_server_data(server, debug=False): + status = fetch_server_status(server.ip, server.port, debug) + s_json = { + "name": server.name, "ip_port": f"{server.ip}:{server.port}", "protocol": 0, + "version_major": 0, "version_minor": 0, "version_patch": 0, "version_full": "N/A", + "players": 0, "max_players": 0, "map": "Offline/Unknown", "gamemode": "UNKNOWN", + "mastermode": "UNKNOWN", "time_left_seconds": -1, "time_left_formatted": "N/A", + "mutators": [], "branch": server.branch, "location": "Unknown", "player_list_data": [] + } + + if status: + # Version logic + is_above_200 = False + if (status.major_version > 2) or \ + (status.major_version == 2 and status.minor_version > 0) or \ + (status.major_version == 2 and status.minor_version == 0 and status.patch_version > 0): + is_above_200 = True + + p_list = [] + for p in status.players: + p_int, p_hex, t_int, t_hex = extract_colors(p.raw_name) + + # Color/team logic + final_team_hex = t_hex + final_team_int = t_int + final_css = generate_css_filter(p_hex) + team_val = "" + + p_hex_clean = p_hex.lstrip('#').lower() + t_hex_clean = t_hex.lstrip('#').lower() + + if is_above_200: + if p_hex_clean == "808080": + # Neutral -> Grey #808080 + final_team_hex, final_team_int = "#808080", 0x808080 + final_css = generate_css_filter(final_team_hex) + team_val = "neutral" + elif p_hex_clean == "f03030": + # Omega -> Red #F03030 + final_team_hex, final_team_int = "#f03030", 0xF03030 + final_css = generate_css_filter(final_team_hex) + team_val = "omega" + elif p_hex_clean == "3030f0": + # Alpha -> Blue #3030F0 + final_team_hex, final_team_int = "#3030f0", 0x3030F0 + final_css = generate_css_filter(final_team_hex) + team_val = "alpha" + else: + if t_hex_clean in ["707070", "90a090"]: + # Neutral -> Grey #808080 + final_team_hex, final_team_int = "#808080", 0x808080 + final_css = generate_css_filter(final_team_hex) + team_val = "neutral" + elif t_hex_clean in ["ff3210", "ff4f44"]: + # Omega -> Red #F03030 + final_team_hex, final_team_int = "#f03030", 0xF03030 + final_css = generate_css_filter(final_team_hex) + team_val = "omega" + elif t_hex_clean in ["1040f8", "5f66ff"]: + # Alpha -> Blue #3030F0 + final_team_hex, final_team_int = "#3030f0", 0x3030F0 + final_css = generate_css_filter(final_team_hex) + team_val = "alpha" + + p_list.append({ + "name": p.name, "privilege": p.privilege, "raw_name": p.raw_name, + "raw_player_int": p_int, "player_color_hex": p_hex, "player_color_css": final_css, + "raw_team_int": final_team_int, "team_color_hex": final_team_hex, "team": team_val + }) + + v_full = f"{status.major_version}.{status.minor_version}.{status.patch_version}" if status.version >= 226 else "N/A" + s_json.update({ + "protocol": status.version, "version_major": status.major_version, + "version_minor": status.minor_version, "version_patch": status.patch_version, + "version_full": v_full, "players": status.clients, "max_players": status.max_clients, + "map": status.map_name, "gamemode": status.game_mode.upper(), "mastermode": status.master_mode, + "time_left_seconds": status.time_left, + "time_left_formatted": time.strftime('%H:%M:%S', time.gmtime(max(0, status.time_left))), + "mutators": get_mutator_names(status.mutators, status.game_mode), + "branch": status.branch or server.branch, "player_list_data": p_list + }) + + # Use cached IP lookup + s_json['location'] = ip_to_country(server.ip) + return s_json + +# ========================================== +# MAIN +# ========================================== + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument('-d', '--debug', action='store_true') + global ARGS + ARGS = parser.parse_args() + + # --- Check Discord Connection (Buffered Output) --- + discord_debug_result = None + + DATA_DIR.mkdir(parents=True, exist_ok=True) + + # --- 0. Load IP cache --- + if IP_CACHE_FILE.exists(): + try: + with open(IP_CACHE_FILE, 'r', encoding='utf-8') as f: + IP_CACHE.update(json.load(f)) + if ARGS.debug: print(f"[DEBUG-IP-Cache] Loaded {len(IP_CACHE)} cached locations.") + except Exception as e: + if ARGS.debug: print(f"[DEBUG-IP-Cache] Failed to load IP cache: {e}") + + # --- 1. Generate maps JSON --- + if MAPS_DIR.exists(): + maps = sorted([f.stem.lower() for f in MAPS_DIR.glob('*.png')]) + with open(MAPS_FILE, 'w') as f: json.dump(maps, f, indent=4) + + # --- 2. Fetch master list --- + try: + with socket.create_connection((MASTER_HOST, MASTER_PORT), TIMEOUT_TCP) as sock: + sock.sendall(b"update\n") + data = b'' + while True: + chunk = sock.recv(4096) + if not chunk: break + data += chunk + raw_list = data.decode('utf-8', errors='ignore') + except Exception as e: + print(f"Failed to fetch master list: {e}") + return + + servers = [] + for line in raw_list.splitlines(): + if line.startswith("addserver "): + p = line.split(' ', 4) + if len(p) >= 5: + res = shlex.split(p[4].strip()) + if len(res) >= 4: + servers.append(Server(p[1], int(p[2]), uncolor_string(res[0]), res[3])) + + # --- 3. Process Servers (UDP) --- + final = [process_server_data(s, ARGS.debug) for s in servers] + final.sort(key=lambda s: (-s.get('players', 0), s.get('name', ''))) + + # --- 4. Notification logic --- + existing_notify = {} + if Path(NOTIFY_FILE).exists(): + try: + with open(NOTIFY_FILE, 'r', encoding='utf-8') as f: + old_data = json.load(f) + for entry in old_data: + key = f"{entry['name']}|{entry['server']}" + existing_notify[key] = entry['jointime'] + except json.JSONDecodeError: + if ARGS.debug: print("Warning: notify.json was corrupted. Resetting notification history.") + existing_notify = {} + except Exception as e: + if ARGS.debug: print(f"Error loading existing notify data: {e}") + + notify_list = [] + utc_now = int(datetime.now(timezone.utc).timestamp()) + + # --- Version logic --- + target_major = -1 + target_minor = -1 + target_patch = -1 + + found_master_by_name = False + best_ver_tuple = (-1, -1, -1) + + target_ip_port = "" + try: + master_ip = socket.gethostbyname(MASTER_HOST) + target_ip_port = f"{master_ip}:{MASTER_PORT}" + except Exception as e: + if ARGS.debug: print(f"Could not resolve Master Host IP: {e}") + + for s in final: + s_maj = s.get('version_major', 0) + s_min = s.get('version_minor', 0) + s_pat = s.get('version_patch', 0) + current_tuple = (s_maj, s_min, s_pat) + + if current_tuple > best_ver_tuple and current_tuple != (0,0,0): + best_ver_tuple = current_tuple + + match_by_ip = (s.get('ip_port') == target_ip_port) + match_by_name = (MASTER_HOST in s.get('name', '')) + + if match_by_ip or match_by_name: + target_major = s_maj + target_minor = s_min + target_patch = s_pat + found_master_by_name = True + if ARGS.debug: + method = "IP" if match_by_ip else "Name-Substring" + print(f"[DEBUG-Version-logic] Found Master Server ({method}): {s.get('name')} -> {target_major}.{target_minor}.{target_patch}") + break + + if not found_master_by_name and best_ver_tuple != (-1, -1, -1): + target_major, target_minor, target_patch = best_ver_tuple + if ARGS.debug: print(f"[DEBUG-Version-logic] Master not found. Fallback to highest version: {target_major}.{target_minor}.{target_patch}") + + for server_data in final: + s_maj = server_data.get('version_major', 0) + s_min = server_data.get('version_minor', 0) + s_pat = server_data.get('version_patch', 0) + + if s_maj == 0 and s_min == 0 and s_pat == 0: + continue + + if target_major != -1 and (s_maj == target_major and s_min == target_minor and s_pat == target_patch): + server_name = server_data.get('name', 'Unknown') + if 'player_list_data' in server_data: + for player in server_data['player_list_data']: + p_name = player.get('name', 'Unknown') + session_key = f"{p_name}|{server_name}" + + if session_key in existing_notify: + join_time = existing_notify[session_key] + else: + join_time = utc_now + # --- Discord bot trigger --- + if DISCORD_BOT == 1: + send_discord_webhook(p_name, server_data) + + map_name = server_data.get('map', 'Unknown') + mode_name = server_data.get('gamemode', 'UNKNOWN') + notify_list.append({ + "name": p_name, + "server": server_name, + "jointime": join_time, + "map": map_name, + "mode": mode_name + }) + + # --- Prepare Discord debug using real data --- + if ARGS.debug: + sample_player = None + sample_server_data = None + + if notify_list: + sample_player = notify_list[0]['name'] + s_name = notify_list[0]['server'] + # Find the full server object + for s in final: + if s.get('name') == s_name: + sample_server_data = s + break + else: + # Fallback: search any server for a player + for s in final: + pld = s.get('player_list_data', []) + if pld: + sample_player = pld[0].get('name', 'Unknown') + sample_server_data = s + break + + if sample_player and sample_server_data: + discord_debug_result = run_discord_debug_check(sample_player, sample_server_data) + else: + discord_debug_result = "[DEBUG-Discord-weebhook] Skipped: No players found to test webhook." + + # --- 5. Save output --- + try: + with open(IP_CACHE_FILE, 'w', encoding='utf-8') as f: + json.dump(IP_CACHE, f, indent=4) + + notify_str = json.dumps(notify_list, indent=4) + with open(NOTIFY_FILE, 'w', encoding='utf-8') as f: + f.write(notify_str) + + output_str = json.dumps(final, indent=4) + with open(OUTPUT_FILE, 'w', encoding='utf-8') as f: + f.write(output_str) + + time_str = json.dumps({ + "local_time": datetime.now().strftime('%d.%m.%Y %H:%M:%S'), + "utc_time": datetime.now(timezone.utc).strftime('%d.%m.%Y %H:%M:%S') + }, indent=4) + with open(TIME_FILE, 'w') as f: + f.write(time_str) + + # --- Print Discord debug info at the very end --- + if discord_debug_result: + print(discord_debug_result) + except Exception as e: + print(f"Failed to write output files: {e}") + +if __name__ == "__main__": + main() diff --git a/includes/footer.html b/includes/footer.html index 35e8f0e..4c35a99 100755 --- a/includes/footer.html +++ b/includes/footer.html @@ -1,6 +1,4 @@ diff --git a/includes/head.html b/includes/head.html index 0bfc15c..e53434d 100755 --- a/includes/head.html +++ b/includes/head.html @@ -1,10 +1,18 @@ + + + + + + + + diff --git a/includes/nav.html b/includes/nav.html index c808aff..932be5d 100755 --- a/includes/nav.html +++ b/includes/nav.html @@ -7,7 +7,7 @@ - {{ site.title | default: site.github.repository_name }} + {{ site.title | default: site.github.repository_name }} diff --git a/includes/release.md b/includes/release.md index 4d8db50..78fb274 100644 --- a/includes/release.md +++ b/includes/release.md @@ -1,4 +1,4 @@ -Red Eclipse 2 brings massive changes to all facets of the game, and the numerous improvements and changes are too numerous to list individually. The broad brush changes to the game are instead enumerated below: +Red Eclipse brings massive changes to all facets of the game, and the numerous improvements and changes are too numerous to list individually. The broad brush changes to the game are instead enumerated below: ### Tesseract Engine @@ -20,12 +20,12 @@ The new new new extended UI system (nnnxui) brings significant improvements to t ### Focused Gameplay -Unlike prior versions of the game with huge quantities of maps, Red Eclipse 2 focuses on providing the best possible levels to play on, at the expense of map volume. There are seven main deathmatch maps included with Red Eclipse 2 and one race map; this is multiple times fewer maps than RE 1.x has shipped. +Unlike prior versions of the game with huge quantities of maps, Red Eclipse focuses on providing the best possible levels to play on, at the expense of map volume. There are seven main deathmatch maps included with Red Eclipse and one race map; this is multiple times fewer maps than RE 1.x has shipped. There is also a focus on making the core gameplay more accessible to new players, with a tutorial level and interactive setup menu to get players to understand the interfaces of the game better. In addition to this, the user interface has been simplified to get players to quickly be able to play standard modes without having to know too much about the sausage making behind the scenes. -The weapons in Red Eclipse 2 have also been significantly revamped compared to 1.6. While the same number of weapons remain with similar names, the weapons have all been retuned to better suit the changes in physics and the addition of pickups. The inclusion of pickups allows for more potent weapons that can be put at a disadvantage by their ammo dependence. +The weapons in Red Eclipse have also been significantly revamped compared to 1.6. While the same number of weapons remain with similar names, the weapons have all been retuned to better suit the changes in physics and the addition of pickups. The inclusion of pickups allows for more potent weapons that can be put at a disadvantage by their ammo dependence. ### Steam Integration -Red Eclipse 2 launches from Steam, a platform few open source games manage to launch on (and no prior Red Eclipse game, for that matter). In doing so, Red Eclipse 2 is easier to install than ever and reaches a broader base than the Linux-heavy community that Red Eclipse 1.x drew upon. With Red Eclipse's permissive licensing scheme, the GPL conflicts with proprietary integration do not exist and it is possible to provide a well integrated game with the Steam ecosystem. +Red Eclipse launches from Steam, a platform few open source games manage to launch on (and no prior Red Eclipse game, for that matter). In doing so, Red Eclipse is easier to install than ever and reaches a broader base than the Linux-heavy community that Red Eclipse 1.x drew upon. With Red Eclipse's permissive licensing scheme, the GPL conflicts with proprietary integration do not exist and it is possible to provide a well integrated game with the Steam ecosystem. diff --git a/index.html b/index.html index 4a97d95..41af2b8 100755 --- a/index.html +++ b/index.html @@ -5,41 +5,396 @@ redirect_from: - /home --- -
-
-

A free arena shooter

-

Fun for everyone, young and old, noob or expert

-

Available for Windows and Linux

-

Parkour, impulse boosts, dashing, and other tricks

-

A huge amount of mutators and game altering variables

-

Create maps with other players in realtime co-op edit

-

Over ten years of active development, and still going!

-
- -

FREE DOWNLOAD

-

v{{ site.data.game.version }} ({{ site.data.game.release }}) released {{ site.data.game.date }}

+ +
+
+
+ {{ site.title }} + +

+ Install {{ site.title }} without Steam +

+

A free

+

arena shooter

+
+

Accessible to everyone

+

Available for Windows, Linux and BSD

+

A movement system with a very high skill ceiling

+

A huge amount of mutators and game altering variables

+

Create maps with other players, in real-time coop

+

Over {{ site.time | date: "%Y" | minus: 2009 }} years of active development

+
+
-
-
- -
+
+