Initial merge of Case Cafe 1.4.1 into mainline

This commit is contained in:
oldmud0 2018-11-09 23:04:19 -06:00
commit c7a58f89b1
68 changed files with 7627 additions and 578 deletions

19
.gitignore vendored
View File

@ -7,3 +7,22 @@ base_override.h
base-full/
bass.lib
bins/
release/
debug/
.qmake.stash
Makefile*
object_script*
/Attorney_Online_remake_resource.rc
/attorney_online_remake_plugin_import.cpp
server/__pycache__
discord/
*.o
moc*
/Attorney_Online_CC_resource.rc
/attorney_online_cc_plugin_import.cpp

View File

@ -10,7 +10,7 @@ greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
RC_ICONS = logo.ico
TARGET = Attorney_Online_remake
TARGET = Attorney_Online
TEMPLATE = app
VERSION = 2.4.10.0
@ -48,7 +48,10 @@ SOURCES += main.cpp\
aolineedit.cpp \
aotextedit.cpp \
aoevidencedisplay.cpp \
discord_rich_presence.cpp
discord_rich_presence.cpp \
aooptionsdialog.cpp \
chatlogpiece.cpp \
aocaseannouncerdialog.cpp
HEADERS += lobby.h \
aoimage.h \
@ -78,12 +81,16 @@ HEADERS += lobby.h \
aotextedit.h \
aoevidencedisplay.h \
discord_rich_presence.h \
discord-rpc.h
discord-rpc.h \
aooptionsdialog.h \
text_file_functions.h \
chatlogpiece.h \
aocaseannouncerdialog.h
# You need to compile the Discord Rich Presence SDK separately and add the lib/headers.
# Discord RPC uses CMake, which does not play nicely with QMake, so this step must be manual.
unix:LIBS += -L$$PWD -ldiscord-rpc
unix:LIBS += -L$$PWD -ldiscord-rpc -lbass
win32:LIBS += -L$$PWD -ldiscord-rpc #"$$PWD/discord-rpc.dll"
CONFIG += c++11

129
README.md
View File

@ -1,3 +1,132 @@
# Attorney Online 2: Case Café Custom Client (AO2:CCCC)
This project is a custom client made specifically for the Case Café server of Attorney Online 2. Nevertheless, the client itself has a bunch of features that are server independent, and if you so wish to run a server with the additional features, get yourself a copy of `tsuserver3`, and replace its `server/` folder with the one supplied here.
Building the project is... complicated. I'm not even sure what I'm doing myself, most of the time. Still, get yourself Qt Creator, and compile the project using that, that's the easiest method of doing things.
Alternatively, you may wait till I make some stuff, and release a compiled executable. You may find said executables in the 'Tags' submenu to the left.
## Features
- **Inline colouring:** allows you to change the text's colour midway through the text.
- `()` (parentheses) will make the text inbetween them blue.
- \` (backwards apostrophes) will make the text green.
- `|` (straight lines) will make the text orange.
- `[]` (square brackets) will make the text grey.
- No need for server support: the clients themselves will interpret these.
- **Additional text features:**
- Type `{` to slow down the text a bit. This takes effect after the character has been typed, so the text may take up different speeds at different points.
- Type `}` to do the opposite! Similar rules apply.
- Both of these can be stacked up to three times, and even against eachother.
- As an example, here is a text:
```
Hello there! This text goes at normal speed.} Now, it's a bit faster!{ Now, it's back to normal.}}} Now it goes at maximum speed! {{Now it's only a little bit faster than normal.
```
- If you begin a message with `~~` (two tildes), those two tildes will be removed, and your message will be centered.
- **Use the in-game settings button:**
- If the theme supports it, you may have a Settings button on the client now, but you can also just type `/settings` in the OOC.
- Modify the contents of your `config.ini` and `callwords.ini` from inside the game!
- Some options may need a restart to take effect.
- **Custom Discord RPC icon and name!**
- **Enhanced character selection screen:**
- The game preloads the characters' icons available on the server, avoiding lag on page switch this way.
- As a side-effect of this, characters can now easily be filtered down to name, whether they are passworded, and if they're taken.
- **Server-supported features:** These will require the modifications in the `server/` folder applied to the server.
- Call mod reason: allows you to input a reason for your modcall.
- Modcalls can be cancelled, if needed.
- Shouts can be disabled serverside (in the sense that they can still interrupt text, but will not make a sound or make the bubble appear).
- The characters' shownames can be changed.
- This needs the server to specifically approve it in areas.
- The client can also turn off the showing of changed shownames if someone is maliciously impersonating someone.
- Any character in the 'jud' position can make a Guilty / Not Guilty text appear with the new button additions.
- These work like the WT / CE popups.
- Capitalisation ignored for server commands. `/getarea` is exactly the same as `/GEtAreA`!
- Various quality-of-life changes for mods, like `/m`, a server-wide mods-only chat.
- Disallow blankposting using `/allow_blankposting`.
- Avoid cucking by setting a jukebox using `/jukebox_toggle`.
- Check the contents of the jukbox with `/jukebox`.
- If you're a mod or the CM, skip the current jukebox song using `/jukebox_skip`.
- Pair up with someone else!
- If two people select eachother's character's character ID using the in-game pair button, or with `/pair [id]`, they will appear on the same screen (assuming they're both in the same position).
- When you appear alongside someone else, you can offset your character to either side using the in-game spinbox, or with `/offset [percentage]`. The percentage can go from -100% (one whole screen's worth to the left) to 100% (one whole screen's worth to the right).
- Areas can have multiple CMs, and these CMs can be anywhere on the server!
- CMs away from the areas they CM in can still see IC and OOC messages coming from there.
- They can also remotely send messages with the `/a [area_id]` command (works both IC and OOC!) or the `/s` command, if they want to message all areas.
- A CM can add other CMs using `/cm [id]`.
- Tired of waiting for pres to finish? Try non-interrupting pres!
- Tired of waiting for OTHERS' pres to finish? `/force_nonint_pres` that thing!
- Also tired of filling evidence up one-by-one? Try `/load_case`!
- Additional juror and seance positions for your RPing / casing needs.
- Areas can be set to locked and spectatable.
- Spectatable areas (using `/area_spectate`) allow people to join, but not talk if they're not on the invite list.
- Locked areas (using `/area_lock`) forbid people not on the invite list from even entering.
- Can't find people to case with? Try the case alert system!
-
- **Area list:**
- The client automatically filters out areas from music if applicable, and these appear in their own list.
- Use the in-game A/M button, or the `/switch_am` command to switch between them.
- If the server supports it, you can even get constant updates about changes in the areas, like players leaving, CMs appearing, statuses changing, etc.
- **Features not mentioned in here?**
- Check the link given by the `/help` function.
- Alternatively, assuming you're reading this on the Github page, browse the wiki!
## Modifications that need to be done
Since this custom client, and the server files supplied with it, add a few features not present in Vanilla, some modifications need to be done to ensure that you can use the full extent of them all. These are as follows:
- **In `areas.yaml`:** (assuming you are the server owner)
- You may add `shouts_allowed` to any of the areas to enable / disable shouts (and judge buttons, and realisation). By default, it's `shouts_allowed: true`.
- You may add `jukebox` to any of the areas to enable the jukebox in there, but you can also use `/jukebox_toggle` in game as a mod to do the same thing. By default, it's `jukebox: false`.
- You may add `showname_changes_allowed` to any of the areas to allow custom shownames used in there. If it's forbidden, players can't send messages or change music as long as they have a custom name set. By default, it's `showname_changes_allowed: false`.
- You may add `abbreviation` to override the server-generated abbreviation of the area. Instead of area numbers, this server-pack uses area abbreviations in server messages for easier understanding (but still uses area IDs in commands, of course). No default here, but here is an example: `abbreviation: SIN` gives the area the abbreviation of 'SIN'.
- You may add `noninterrupting_pres` to force users to use non-interrupting pres only. CCCC users will see the pres play as the text goes, Vanilla users will not see pres at all. The default is `noninterrupting_pres: false`.
- **In your themes:**
- You'll need the following, additional images:
- `notguilty.gif`, which is a gif of the Not Guilty verdict being given.
- `guilty.gif`, which is a gif of the Guilty verdict being given.
- `notguilty.png`, which is a static image for the button for the Not Guilty verdict.
- `guilty.png`, which is a static image for the button for the Guilty verdict.
- `pair_button.png`, which is a static image for the Pair button, when it isn't pressed.
- `pair_button_pressed.png`, which is the same, but for when the button is pressed.
- In your `lobby_design.ini`:
- Extend the width of the `version` label to a bigger size. Said label now shows both the underlying AO's version, and the custom client's version.
- In your `courtroom_sounds.ini`:
- Add a sound effect for `not_guilty`, for example: `not_guilty = sfx-notguilty.wav`.
- Add a sound effect for `guilty`, for example: `guilty = sfx-guilty.wav`.
- Add a sound effect for the case alerts. They work similarly to modcall alerts, or callword alerts. For example: `case_call = sfx-triplegavel-soj.wav`.
- In your `courtroom_design.ini`, place the following new UI elements as and if you wish:
- `log_limit_label`, which is a simple text that exmplains what the spinbox with the numbers is. Needs an X, Y, width, height number.
- `log_limit_spinbox`, which is the spinbox for the log limit, allowing you to set the size of the log limit in-game. Needs the same stuff as above.
- `ic_chat_name`, which is an input field for your custom showname. Needs the same stuff.
- `ao2_ic_chat_name`, which is the same as above, but comes into play when the background has a desk.
- Further comments on this: all `ao2_` UI elements come into play when the background has a desk. However, in AO2 nowadays, it's customary for every background to have a desk, even if it's just an empty gif. So you most likely have never seen the `ao2_`-less UI elements ever come into play, unless someone mis-named a desk or something.
- `showname_enable` is a tickbox that toggles whether you should see shownames or not. This does not influence whether you can USE custom shownames or not, so you can have it off, while still showing a custom showname to everyone else. Needs X, Y, width, height as usual.
- `settings` is a plain button that takes up the OS's looks, like the 'Call mod' button. Takes the same arguments as above.
- You can also just type `/settings` in OOC.
- `char_search` is a text input box on the character selection screen, which allows you to filter characters down to name. Needs the same arguments.
- `char_passworded` is a tickbox, that when ticked, shows all passworded characters on the character selection screen. Needs the same as above.
- `char_taken` is another tickbox, that does the same, but for characters that are taken.
- `not_guilty` is a button similar to the CE / WT buttons, that if pressed, plays the Not Guilty verdict animation. Needs the same arguments.
- `guilty` is similar to `not_guilty`, but for the Guilty verdict.
- `pair_button` is a toggleable button, that shows and hides the pairing list and the offset spinbox. Works similarly to the mute button.
- `pair_list` is a list of all characters in alphabetical order, shown when the user presses the Pair button. If a character is clicked on it, it is selected as the character the user wants to pair up with.
- `pair_offset_spinbox` is a spinbox that allows the user to choose between offsets of -100% to 100%.
- `switch_area_music` is a button with the text 'A/M', that toggles between the music list and the areas list. Though the two are different, they are programmed to take the same space.
- `pre_no_interrupt` is a tickbox with the text 'No Intrpt', that toggles whether preanimations should delay the text or not.
- `area_free_color` is a combination of red, green, and blue values ranging from 0 to 255. This determines the colour of the area in the Area list if it's free, and has a status of IDLE.
- `area_lfp_color` determines the colour of the area if its status is LOOKING-FOR-PLAYERS.
- `area_casing_color` determines the colour of the area if its status is CASING.
- `area_recess_color` determines the colour of the area if its status is RECESS.
- `area_rp_color` determines the colour of the area if its status is RP.
- `area_gaming_color` determines the colour of the area if its status is GAMING.
- `area_locked_color` determines the colour of the area if it is locked, REGARDLESS of status.
- `ooc_default_color` determines the colour of the username in the OOC chat if the message doesn't come from the server.
- `ooc_server_color` determines the colour of the username if the message arrived from the server.
- `casing_button` is a button with the text 'Casing' that when clicked, brings up the Case Announcements dialog. You can give the case a name, and tick whom do you want to alert. You need to be a CM for it to go through. Only people who have at least one of the roles ticked will get the alert.
- `casing` is a tickbox with the text 'Casing'. If ticked, you will get the case announcements alerts you should get, in accordance to the above. In the settings, you can change your defaults on the 'Casing' tab. (That's a buncha things titled 'Casing'!)
---
# Attorney-Online-Client-Remake
This is a open-source remake of Attorney Online written by OmniTroid. The original Attorney Online client was written by FanatSors in Delphi.

View File

@ -1,79 +0,0 @@
<?xml version="1.0"?>
<manifest package="com.aceattorneyonline.ao2" xmlns:android="http://schemas.android.com/apk/res/android" android:versionName="2.3.5.0" android:versionCode="2" android:installLocation="auto">
<application android:hardwareAccelerated="true" android:name="org.qtproject.qt5.android.bindings.QtApplication" android:label="Attorney Online 2" android:icon="@drawable/icon">
<activity android:configChanges="orientation|uiMode|screenLayout|screenSize|smallestScreenSize|layoutDirection|locale|fontScale|keyboard|keyboardHidden|navigation" android:name="org.qtproject.qt5.android.bindings.QtActivity" android:label="Attorney Online 2 Lobby" android:screenOrientation="unspecified" android:launchMode="singleTop">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<!-- Application arguments -->
<!-- meta-data android:name="android.app.arguments" android:value="arg1 arg2 arg3"/ -->
<!-- Application arguments -->
<meta-data android:name="android.app.lib_name" android:value="-- %%INSERT_APP_LIB_NAME%% --"/>
<meta-data android:name="android.app.qt_sources_resource_id" android:resource="@array/qt_sources"/>
<meta-data android:name="android.app.repository" android:value="default"/>
<meta-data android:name="android.app.qt_libs_resource_id" android:resource="@array/qt_libs"/>
<meta-data android:name="android.app.bundled_libs_resource_id" android:resource="@array/bundled_libs"/>
<!-- Deploy Qt libs as part of package -->
<meta-data android:name="android.app.bundle_local_qt_libs" android:value="-- %%BUNDLE_LOCAL_QT_LIBS%% --"/>
<meta-data android:name="android.app.bundled_in_lib_resource_id" android:resource="@array/bundled_in_lib"/>
<meta-data android:name="android.app.bundled_in_assets_resource_id" android:resource="@array/bundled_in_assets"/>
<!-- Run with local libs -->
<meta-data android:name="android.app.use_local_qt_libs" android:value="-- %%USE_LOCAL_QT_LIBS%% --"/>
<meta-data android:name="android.app.libs_prefix" android:value="/data/local/tmp/qt/"/>
<meta-data android:name="android.app.load_local_libs" android:value="-- %%INSERT_LOCAL_LIBS%% --"/>
<meta-data android:name="android.app.load_local_jars" android:value="-- %%INSERT_LOCAL_JARS%% --"/>
<meta-data android:name="android.app.static_init_classes" android:value="-- %%INSERT_INIT_CLASSES%% --"/>
<!-- Messages maps -->
<meta-data android:value="@string/ministro_not_found_msg" android:name="android.app.ministro_not_found_msg"/>
<meta-data android:value="@string/ministro_needed_msg" android:name="android.app.ministro_needed_msg"/>
<meta-data android:value="@string/fatal_error_msg" android:name="android.app.fatal_error_msg"/>
<!-- Messages maps -->
<!-- Splash screen -->
<!-- meta-data android:name="android.app.splash_screen_drawable" android:resource="@drawable/logo"/ -->
<!-- meta-data android:name="android.app.splash_screen_sticky" android:value="true"/ -->
<!-- Splash screen -->
<!-- Background running -->
<!-- Warning: changing this value to true may cause unexpected crashes if the
application still try to draw after
"applicationStateChanged(Qt::ApplicationSuspended)"
signal is sent! -->
<meta-data android:name="android.app.background_running" android:value="false"/>
<!-- Background running -->
<!-- auto screen scale factor -->
<meta-data android:name="android.app.auto_screen_scale_factor" android:value="false"/>
<!-- auto screen scale factor -->
<!-- extract android style -->
<!-- available android:values :
* full - useful QWidget & Quick Controls 1 apps
* minimal - useful for Quick Controls 2 apps, it is much faster than "full"
* none - useful for apps that don't use any of the above Qt modules
-->
<meta-data android:name="android.app.extract_android_style" android:value="full"/>
<!-- extract android style -->
</activity>
<!-- For adding service(s) please check: https://wiki.qt.io/AndroidServices -->
</application>
<uses-sdk android:minSdkVersion="16" android:targetSdkVersion="23"/>
<supports-screens android:largeScreens="true" android:normalScreens="true" android:anyDensity="true" android:smallScreens="true"/>
<!-- The following comment will be replaced upon deployment with default permissions based on the dependencies of the application.
Remove the comment if you do not require these default permissions. -->
<!-- The following comment will be replaced upon deployment with default features based on the dependencies of the application.
Remove the comment if you do not require these default features. -->
<!-- %%INSERT_FEATURES -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

View File

@ -1 +0,0 @@
target=android-21

View File

@ -5,8 +5,14 @@
#include "networkmanager.h"
#include "debug_functions.h"
#include "aooptionsdialog.h"
#include "aocaseannouncerdialog.h"
AOApplication::AOApplication(int &argc, char **argv) : QApplication(argc, argv)
{
// Create the QSettings class that points to the config.ini.
configini = new QSettings(get_base_path() + "config.ini", QSettings::IniFormat);
net_manager = new NetworkManager(this);
discord = new AttorneyOnline::Discord();
QObject::connect(net_manager, SIGNAL(ms_connect_finished(bool, bool)),
@ -36,7 +42,7 @@ void AOApplication::construct_lobby()
int y = (screenGeometry.height()-w_lobby->height()) / 2;
w_lobby->move(x, y);
if(is_discord_enabled())
if (is_discord_enabled())
discord->state_lobby();
w_lobby->show();
@ -91,6 +97,14 @@ QString AOApplication::get_version_string()
QString::number(MINOR_VERSION);
}
QString AOApplication::get_cccc_version_string()
{
return
QString::number(CCCC_RELEASE) + "." +
QString::number(CCCC_MAJOR_VERSION) + "." +
QString::number(CCCC_MINOR_VERSION);
}
void AOApplication::reload_theme()
{
current_theme = read_theme();
@ -164,3 +178,18 @@ void AOApplication::ms_connect_finished(bool connected, bool will_retry)
}
}
}
void AOApplication::call_settings_menu()
{
AOOptionsDialog* settings = new AOOptionsDialog(nullptr, this);
settings->exec();
delete settings;
}
void AOApplication::call_announce_menu(Courtroom *court)
{
AOCaseAnnouncerDialog* announcer = new AOCaseAnnouncerDialog(nullptr, this, court);
announcer->exec();
delete announcer;
}

View File

@ -8,10 +8,21 @@
#include <QApplication>
#include <QVector>
#include <QFile>
#include <QSettings>
#include <QDebug>
#include <QRect>
#include <QDesktopWidget>
#include <QCryptographicHash>
#include <QDir>
#include <QStandardPaths>
#include <QTextStream>
#include <QStringList>
#include <QColor>
class NetworkManager;
class Lobby;
class Courtroom;
@ -44,6 +55,9 @@ public:
void send_ms_packet(AOPacket *p_packet);
void send_server_packet(AOPacket *p_packet, bool encoded = true);
void call_settings_menu();
void call_announce_menu(Courtroom *court);
/////////////////server metadata//////////////////
unsigned int s_decryptor = 5;
@ -56,6 +70,10 @@ public:
bool improved_loading_enabled = false;
bool desk_mod_enabled = false;
bool evidence_enabled = false;
bool cccc_ic_support_enabled = false;
bool arup_enabled = false;
bool casing_alerts_enabled = false;
bool modcall_reason_enabled = false;
///////////////loading info///////////////////
@ -66,6 +84,7 @@ public:
int char_list_size = 0;
int loaded_chars = 0;
int generated_chars = 0;
int evidence_list_size = 0;
int loaded_evidence = 0;
int music_list_size = 0;
@ -80,6 +99,11 @@ public:
int get_minor_version() {return MINOR_VERSION;}
QString get_version_string();
int get_cccc_release() {return CCCC_RELEASE;}
int get_cccc_major_version() {return CCCC_MAJOR_VERSION;}
int get_cccc_minor_version() {return CCCC_MINOR_VERSION;}
QString get_cccc_version_string();
///////////////////////////////////////////
void set_favorite_list();
@ -111,8 +135,13 @@ public:
////// Functions for reading and writing files //////
// Implementations file_functions.cpp
// Instead of reinventing the wheel, we'll use a QSettings class.
QSettings *configini;
//Returns the config value for the passed searchline from a properly formatted config ini file
QString read_config(QString searchline);
//QString read_config(QString searchline);
// No longer necessary.
//Reads the theme from config.ini and loads it into the current_theme variable
QString read_theme();
@ -135,11 +164,26 @@ public:
//Returns the value of default_blip in config.ini
int get_default_blip();
//Returns true if discord is enabled in config.ini and false otherwise
// Returns the value of whether Discord should be enabled on startup
// from the config.ini.
bool is_discord_enabled();
//Returns true if reverse IC is enabled in config.ini and false otherwise
bool ic_scroll_down_enabled();
// Returns the value of the maximum amount of lines the IC chatlog
// may contain, from config.ini.
int get_max_log_size();
// Returns whether the log should go upwards (new behaviour)
// or downwards (vanilla behaviour).
bool get_log_goes_downwards();
// Returns the username the user may have set in config.ini.
QString get_default_username();
// Returns the audio device used for the client.
QString get_audio_output_device();
// Returns whether the user would like to have custom shownames on by default.
bool get_showname_enabled_by_default();
//Returns the list of words in callwords.ini
QStringList get_call_words();
@ -165,11 +209,14 @@ public:
//Returns the color with p_identifier from p_file
QColor get_color(QString p_identifier, QString p_file);
// Returns the colour from the misc folder.
QColor get_chat_color(QString p_identifier, QString p_chat);
//Returns the sfx with p_identifier from sounds.ini in the current theme path
QString get_sfx(QString p_identifier);
//Returns the value of p_search_line within target_tag and terminator_tag
QString read_char_ini(QString p_char, QString p_search_line, QString target_tag, QString terminator_tag);
QString read_char_ini(QString p_char, QString p_search_line, QString target_tag);
//Returns the side of the p_char character from that characters ini file
QString get_char_side(QString p_char);
@ -192,6 +239,9 @@ public:
//Not in use
int get_text_delay(QString p_char, QString p_emote);
// Returns the custom realisation used by the character.
QString get_custom_realization(QString p_char);
//Returns the name of p_char
QString get_char_name(QString p_char);
@ -222,11 +272,43 @@ public:
//Returns p_char's gender
QString get_gender(QString p_char);
// ======
// These are all casing-related settings.
// ======
// Returns if the user has casing alerts enabled.
bool get_casing_enabled();
// Returns if the user wants to get alerts for the defence role.
bool get_casing_defence_enabled();
// Same for prosecution.
bool get_casing_prosecution_enabled();
// Same for judge.
bool get_casing_judge_enabled();
// Same for juror.
bool get_casing_juror_enabled();
// Same for steno.
bool get_casing_steno_enabled();
// Same for CM.
bool get_casing_cm_enabled();
// Get the message for the CM for casing alerts.
QString get_casing_can_host_cases();
private:
const int RELEASE = 2;
const int MAJOR_VERSION = 4;
const int MINOR_VERSION = 10;
const int CCCC_RELEASE = 1;
const int CCCC_MAJOR_VERSION = 4;
const int CCCC_MINOR_VERSION = 1;
QString current_theme = "default";
QVector<server_type> server_list;

View File

@ -2,32 +2,46 @@
AOBlipPlayer::AOBlipPlayer(QWidget *parent, AOApplication *p_ao_app)
{
m_sfxplayer = new QSoundEffect;
m_parent = parent;
ao_app = p_ao_app;
}
AOBlipPlayer::~AOBlipPlayer()
{
m_sfxplayer->stop();
m_sfxplayer->deleteLater();
}
void AOBlipPlayer::set_blips(QString p_sfx)
{
m_sfxplayer->stop();
QString f_path = ao_app->get_sounds_path() + p_sfx.toLower();
m_sfxplayer->setSource(QUrl::fromLocalFile(f_path));
for (int n_stream = 0 ; n_stream < 5 ; ++n_stream)
{
BASS_StreamFree(m_stream_list[n_stream]);
m_stream_list[n_stream] = BASS_StreamCreateFile(FALSE, f_path.utf16(), 0, 0, BASS_UNICODE | BASS_ASYNCFILE);
}
set_volume(m_volume);
}
void AOBlipPlayer::blip_tick()
{
m_sfxplayer->play();
int f_cycle = m_cycle++;
if (m_cycle == 5)
m_cycle = 0;
HSTREAM f_stream = m_stream_list[f_cycle];
if (ao_app->get_audio_output_device() != "Default")
BASS_ChannelSetDevice(f_stream, BASS_GetDevice());
BASS_ChannelPlay(f_stream, false);
}
void AOBlipPlayer::set_volume(int p_value)
{
m_volume = p_value;
m_sfxplayer->setVolume(p_value / 100.0);
float volume = p_value / 100.0f;
for (int n_stream = 0 ; n_stream < 5 ; ++n_stream)
{
BASS_ChannelSetAttribute(m_stream_list[n_stream], BASS_ATTRIB_VOL, volume);
}
}

View File

@ -1,18 +1,17 @@
#ifndef AOBLIPPLAYER_H
#define AOBLIPPLAYER_H
#include "bass.h"
#include "aoapplication.h"
#include <QWidget>
#include <string.h>
#include <QDebug>
#include <QSoundEffect>
class AOBlipPlayer
{
public:
AOBlipPlayer(QWidget *parent, AOApplication *p_ao_app);
~AOBlipPlayer();
void set_blips(QString p_sfx);
void blip_tick();
@ -23,9 +22,9 @@ public:
private:
QWidget *m_parent;
AOApplication *ao_app;
QSoundEffect *m_sfxplayer;
int m_volume;
HSTREAM m_stream_list[5];
};
#endif // AOBLIPPLAYER_H

81
aocaseannouncerdialog.cpp Normal file
View File

@ -0,0 +1,81 @@
#include "aocaseannouncerdialog.h"
AOCaseAnnouncerDialog::AOCaseAnnouncerDialog(QWidget *parent, AOApplication *p_ao_app, Courtroom *p_court)
{
ao_app = p_ao_app;
court = p_court;
setWindowTitle("Case Announcer");
resize(405, 235);
AnnouncerButtons = new QDialogButtonBox(this);
QSizePolicy sizepolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
sizepolicy.setHorizontalStretch(0);
sizepolicy.setVerticalStretch(0);
sizepolicy.setHeightForWidth(AnnouncerButtons->sizePolicy().hasHeightForWidth());
AnnouncerButtons->setSizePolicy(sizepolicy);
AnnouncerButtons->setOrientation(Qt::Horizontal);
AnnouncerButtons->setStandardButtons(QDialogButtonBox::Ok|QDialogButtonBox::Cancel);
QObject::connect(AnnouncerButtons, SIGNAL(accepted()), this, SLOT(ok_pressed()));
QObject::connect(AnnouncerButtons, SIGNAL(rejected()), this, SLOT(cancel_pressed()));
setUpdatesEnabled(false);
VBoxLayout = new QVBoxLayout(this);
FormLayout = new QFormLayout(this);
FormLayout->setLabelAlignment(Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter);
FormLayout->setFormAlignment(Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop);
FormLayout->setContentsMargins(6, 6, 6, 6);
VBoxLayout->addItem(FormLayout);
VBoxLayout->addWidget(AnnouncerButtons);
CaseTitleLabel = new QLabel(this);
CaseTitleLabel->setText("Case title:");
FormLayout->setWidget(0, QFormLayout::LabelRole, CaseTitleLabel);
CaseTitleLineEdit = new QLineEdit(this);
CaseTitleLineEdit->setMaxLength(50);
FormLayout->setWidget(0, QFormLayout::FieldRole, CaseTitleLineEdit);
DefenceNeeded = new QCheckBox(this);
DefenceNeeded->setText("Defence needed");
ProsecutorNeeded = new QCheckBox(this);
ProsecutorNeeded->setText("Prosecution needed");
JudgeNeeded = new QCheckBox(this);
JudgeNeeded->setText("Judge needed");
JurorNeeded = new QCheckBox(this);
JurorNeeded->setText("Jurors needed");
StenographerNeeded = new QCheckBox(this);
StenographerNeeded->setText("Stenographer needed");
FormLayout->setWidget(1, QFormLayout::FieldRole, DefenceNeeded);
FormLayout->setWidget(2, QFormLayout::FieldRole, ProsecutorNeeded);
FormLayout->setWidget(3, QFormLayout::FieldRole, JudgeNeeded);
FormLayout->setWidget(4, QFormLayout::FieldRole, JurorNeeded);
FormLayout->setWidget(5, QFormLayout::FieldRole, StenographerNeeded);
setUpdatesEnabled(true);
}
void AOCaseAnnouncerDialog::ok_pressed()
{
court->announce_case(CaseTitleLineEdit->text(),
DefenceNeeded->isChecked(),
ProsecutorNeeded->isChecked(),
JudgeNeeded->isChecked(),
JurorNeeded->isChecked(),
StenographerNeeded->isChecked());
done(0);
}
void AOCaseAnnouncerDialog::cancel_pressed()
{
done(0);
}

45
aocaseannouncerdialog.h Normal file
View File

@ -0,0 +1,45 @@
#ifndef AOCASEANNOUNCERDIALOG_H
#define AOCASEANNOUNCERDIALOG_H
#include "aoapplication.h"
#include "courtroom.h"
#include <QtWidgets/QDialog>
#include <QDialogButtonBox>
#include <QtWidgets/QVBoxLayout>
#include <QtWidgets/QFormLayout>
#include <QtWidgets/QCheckBox>
#include <QtWidgets/QLabel>
#include <QtWidgets/QLineEdit>
class AOCaseAnnouncerDialog : public QDialog
{
Q_OBJECT
public:
explicit AOCaseAnnouncerDialog(QWidget *parent = nullptr, AOApplication *p_ao_app = nullptr, Courtroom *p_court = nullptr);
private:
AOApplication *ao_app;
Courtroom *court;
QDialogButtonBox *AnnouncerButtons;
QVBoxLayout *VBoxLayout;
QFormLayout *FormLayout;
QLabel *CaseTitleLabel;
QLineEdit *CaseTitleLineEdit;
QCheckBox *DefenceNeeded;
QCheckBox *ProsecutorNeeded;
QCheckBox *JudgeNeeded;
QCheckBox *JurorNeeded;
QCheckBox *StenographerNeeded;
public slots:
void ok_pressed();
void cancel_pressed();
};
#endif // AOCASEANNOUNCERDIALOG_H

View File

@ -2,12 +2,14 @@
#include "file_functions.h"
AOCharButton::AOCharButton(QWidget *parent, AOApplication *p_ao_app, int x_pos, int y_pos) : QPushButton(parent)
AOCharButton::AOCharButton(QWidget *parent, AOApplication *p_ao_app, int x_pos, int y_pos, bool is_taken) : QPushButton(parent)
{
m_parent = parent;
ao_app = p_ao_app;
taken = is_taken;
this->resize(60, 60);
this->move(x_pos, y_pos);
@ -40,7 +42,11 @@ void AOCharButton::reset()
void AOCharButton::set_taken()
{
ui_taken->show();
if (taken)
{
ui_taken->move(0,0);
ui_taken->show();
}
}
void AOCharButton::set_passworded()
@ -73,6 +79,7 @@ void AOCharButton::set_image(QString p_character)
void AOCharButton::enterEvent(QEvent * e)
{
ui_selector->move(this->x() - 1, this->y() - 1);
ui_selector->raise();
ui_selector->show();

View File

@ -14,10 +14,11 @@ class AOCharButton : public QPushButton
Q_OBJECT
public:
AOCharButton(QWidget *parent, AOApplication *p_ao_app, int x_pos, int y_pos);
AOCharButton(QWidget *parent, AOApplication *p_ao_app, int x_pos, int y_pos, bool is_taken);
AOApplication *ao_app;
void refresh();
void reset();
void set_taken();
void set_passworded();
@ -25,6 +26,8 @@ public:
void set_image(QString p_character);
private:
bool taken;
QWidget *m_parent;
AOImage *ui_taken;

View File

@ -146,14 +146,30 @@ void AOCharMovie::combo_resize(int w, int h)
m_movie->setScaledSize(f_size);
}
void AOCharMovie::move(int ax, int ay)
{
x = ax;
y = ay;
QLabel::move(x, y);
}
void AOCharMovie::frame_change(int n_frame)
{
if (movie_frames.size() > n_frame)
{
QPixmap f_pixmap = QPixmap::fromImage(movie_frames.at(n_frame));
auto aspect_ratio = Qt::KeepAspectRatio;
this->setPixmap(f_pixmap.scaled(this->width(), this->height()));
}
if (f_pixmap.size().width() > f_pixmap.size().height())
aspect_ratio = Qt::KeepAspectRatioByExpanding;
if (f_pixmap.size().width() > this->size().width() || f_pixmap.size().height() > this->size().height())
this->setPixmap(f_pixmap.scaled(this->width(), this->height(), aspect_ratio, Qt::SmoothTransformation));
else
this->setPixmap(f_pixmap.scaled(this->width(), this->height(), aspect_ratio, Qt::FastTransformation));
QLabel::move(x + (this->width() - this->pixmap()->width())/2, y);
}
if (m_movie->frameCount() - 1 == n_frame && play_once)
{

View File

@ -25,6 +25,8 @@ public:
void stop();
void move(int ax, int ay);
void combo_resize(int w, int h);
private:
@ -36,6 +38,10 @@ private:
const int time_mod = 62;
// These are the X and Y values before they are fixed based on the sprite's width.
int x = 0;
int y = 0;
bool m_flipped = false;
bool play_once = true;

View File

@ -32,13 +32,16 @@ void AOMovie::play(QString p_gif, QString p_char, QString p_custom_theme)
else
custom_path = ao_app->get_character_path(p_char) + p_gif + "_bubble.gif";
QString misc_path = ao_app->get_base_path() + "misc/" + p_custom_theme + "/" + p_gif + "_bubble.gif";
QString custom_theme_path = ao_app->get_base_path() + "themes/" + p_custom_theme + "/" + p_gif + ".gif";
QString theme_path = ao_app->get_theme_path() + p_gif + ".gif";
QString default_theme_path = ao_app->get_default_theme_path() + p_gif + ".gif";
QString placeholder_path = ao_app->get_theme_path() + "placeholder.gif";
QString default_placeholder_path = ao_app->get_default_theme_path() + "placeholder.gif";
if (file_exists(custom_path))
if (file_exists(misc_path))
gif_path = misc_path;
else if (file_exists(custom_path))
gif_path = custom_path;
else if (file_exists(custom_theme_path))
gif_path = custom_theme_path;

View File

@ -4,24 +4,31 @@ AOMusicPlayer::AOMusicPlayer(QWidget *parent, AOApplication *p_ao_app)
{
m_parent = parent;
ao_app = p_ao_app;
m_player = new QMediaPlayer;
}
AOMusicPlayer::~AOMusicPlayer()
{
m_player->stop();
m_player->deleteLater();
BASS_ChannelStop(m_stream);
}
void AOMusicPlayer::play(QString p_song)
{
m_player->setMedia(QUrl::fromLocalFile(ao_app->get_music_path(p_song)));
BASS_ChannelStop(m_stream);
QString f_path = ao_app->get_music_path(p_song);
m_stream = BASS_StreamCreateFile(FALSE, f_path.utf16(), 0, 0, BASS_STREAM_AUTOFREE | BASS_UNICODE | BASS_ASYNCFILE);
this->set_volume(m_volume);
m_player->play();
if (ao_app->get_audio_output_device() != "Default")
BASS_ChannelSetDevice(m_stream, BASS_GetDevice());
BASS_ChannelPlay(m_stream, false);
}
void AOMusicPlayer::set_volume(int p_value)
{
m_volume = p_value;
m_player->setVolume(p_value);
float volume = m_volume / 100.0f;
BASS_ChannelSetAttribute(m_stream, BASS_ATTRIB_VOL, volume);
}

View File

@ -1,12 +1,12 @@
#ifndef AOMUSICPLAYER_H
#define AOMUSICPLAYER_H
#include "bass.h"
#include "aoapplication.h"
#include <QWidget>
#include <string.h>
#include <QDebug>
#include <QMediaPlayer>
class AOMusicPlayer
{
@ -21,8 +21,8 @@ private:
QWidget *m_parent;
AOApplication *ao_app;
QMediaPlayer *m_player;
int m_volume = 0;
HSTREAM m_stream;
};
#endif // AOMUSICPLAYER_H

497
aooptionsdialog.cpp Normal file
View File

@ -0,0 +1,497 @@
#include "aooptionsdialog.h"
#include "aoapplication.h"
#include "bass.h"
AOOptionsDialog::AOOptionsDialog(QWidget *parent, AOApplication *p_ao_app) : QDialog(parent)
{
ao_app = p_ao_app;
// Setting up the basics.
// setAttribute(Qt::WA_DeleteOnClose);
setWindowTitle("Settings");
resize(398, 320);
SettingsButtons = new QDialogButtonBox(this);
QSizePolicy sizePolicy1(QSizePolicy::Expanding, QSizePolicy::Fixed);
sizePolicy1.setHorizontalStretch(0);
sizePolicy1.setVerticalStretch(0);
sizePolicy1.setHeightForWidth(SettingsButtons->sizePolicy().hasHeightForWidth());
SettingsButtons->setSizePolicy(sizePolicy1);
SettingsButtons->setOrientation(Qt::Horizontal);
SettingsButtons->setStandardButtons(QDialogButtonBox::Cancel|QDialogButtonBox::Save);
QObject::connect(SettingsButtons, SIGNAL(accepted()), this, SLOT(save_pressed()));
QObject::connect(SettingsButtons, SIGNAL(rejected()), this, SLOT(discard_pressed()));
// We'll stop updates so that the window won't flicker while it's being made.
setUpdatesEnabled(false);
// First of all, we want a tabbed dialog, so let's add some layout.
verticalLayout = new QVBoxLayout(this);
SettingsTabs = new QTabWidget(this);
verticalLayout->addWidget(SettingsTabs);
verticalLayout->addWidget(SettingsButtons);
// Let's add the tabs one by one.
// First, we'll start with 'Gameplay'.
GameplayTab = new QWidget();
SettingsTabs->addTab(GameplayTab, "Gameplay");
formLayoutWidget = new QWidget(GameplayTab);
formLayoutWidget->setGeometry(QRect(10, 10, 361, 211));
GameplayForm = new QFormLayout(formLayoutWidget);
GameplayForm->setLabelAlignment(Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter);
GameplayForm->setFormAlignment(Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop);
GameplayForm->setContentsMargins(0, 0, 0, 0);
ThemeLabel = new QLabel(formLayoutWidget);
ThemeLabel->setText("Theme:");
ThemeLabel->setToolTip("Allows you to set the theme used ingame. If your theme changes the lobby's look, too, you'll obviously need to reload the lobby somehow for it take effect. Joining a server and leaving it should work.");
GameplayForm->setWidget(0, QFormLayout::LabelRole, ThemeLabel);
ThemeCombobox = new QComboBox(formLayoutWidget);
// Fill the combobox with the names of the themes.
QDirIterator it(p_ao_app->get_base_path() + "themes", QDir::Dirs, QDirIterator::NoIteratorFlags);
while (it.hasNext())
{
QString actualname = QDir(it.next()).dirName();
if (actualname != "." && actualname != "..")
ThemeCombobox->addItem(actualname);
if (actualname == p_ao_app->read_theme())
ThemeCombobox->setCurrentIndex(ThemeCombobox->count()-1);
}
GameplayForm->setWidget(0, QFormLayout::FieldRole, ThemeCombobox);
ThemeLogDivider = new QFrame(formLayoutWidget);
ThemeLogDivider->setMidLineWidth(0);
ThemeLogDivider->setFrameShape(QFrame::HLine);
ThemeLogDivider->setFrameShadow(QFrame::Sunken);
GameplayForm->setWidget(1, QFormLayout::FieldRole, ThemeLogDivider);
DownwardsLabel = new QLabel(formLayoutWidget);
DownwardsLabel->setText("Log goes downwards:");
DownwardsLabel->setToolTip("If ticked, the IC chatlog will go downwards, in the sense that new messages will appear at the bottom (like the OOC chatlog). The Vanilla behaviour is equivalent to this being unticked.");
GameplayForm->setWidget(2, QFormLayout::LabelRole, DownwardsLabel);
DownwardCheckbox = new QCheckBox(formLayoutWidget);
DownwardCheckbox->setChecked(p_ao_app->get_log_goes_downwards());
GameplayForm->setWidget(2, QFormLayout::FieldRole, DownwardCheckbox);
LengthLabel = new QLabel(formLayoutWidget);
LengthLabel->setText("Log length:");
LengthLabel->setToolTip("The amount of messages the IC chatlog will keep before getting rid of older messages. A value of 0 or below counts as 'infinite'.");
GameplayForm->setWidget(3, QFormLayout::LabelRole, LengthLabel);
LengthSpinbox = new QSpinBox(formLayoutWidget);
LengthSpinbox->setMaximum(10000);
LengthSpinbox->setValue(p_ao_app->get_max_log_size());
GameplayForm->setWidget(3, QFormLayout::FieldRole, LengthSpinbox);
LogNamesDivider = new QFrame(formLayoutWidget);
LogNamesDivider->setFrameShape(QFrame::HLine);
LogNamesDivider->setFrameShadow(QFrame::Sunken);
GameplayForm->setWidget(4, QFormLayout::FieldRole, LogNamesDivider);
UsernameLabel = new QLabel(formLayoutWidget);
UsernameLabel->setText("Default username:");
UsernameLabel->setToolTip("Your OOC name will be filled in with this string when you join a server.");
GameplayForm->setWidget(5, QFormLayout::LabelRole, UsernameLabel);
UsernameLineEdit = new QLineEdit(formLayoutWidget);
UsernameLineEdit->setMaxLength(30);
UsernameLineEdit->setText(p_ao_app->get_default_username());
GameplayForm->setWidget(5, QFormLayout::FieldRole, UsernameLineEdit);
ShownameLabel = new QLabel(formLayoutWidget);
ShownameLabel->setText("Custom shownames:");
ShownameLabel->setToolTip("Gives the default value for the ingame 'Custom shownames' tickbox, which in turn determines whether your client should display custom shownames or not.");
GameplayForm->setWidget(6, QFormLayout::LabelRole, ShownameLabel);
ShownameCheckbox = new QCheckBox(formLayoutWidget);
ShownameCheckbox->setChecked(p_ao_app->get_showname_enabled_by_default());
GameplayForm->setWidget(6, QFormLayout::FieldRole, ShownameCheckbox);
NetDivider = new QFrame(formLayoutWidget);
NetDivider->setFrameShape(QFrame::HLine);
NetDivider->setFrameShadow(QFrame::Sunken);
GameplayForm->setWidget(7, QFormLayout::FieldRole, NetDivider);
MasterServerLabel = new QLabel(formLayoutWidget);
MasterServerLabel->setText("Backup MS:");
MasterServerLabel->setToolTip("After the built-in server lookups fail, the game will try the address given here and use it as a backup masterserver address.");
GameplayForm->setWidget(8, QFormLayout::LabelRole, MasterServerLabel);
QSettings* configini = ao_app->configini;
MasterServerLineEdit = new QLineEdit(formLayoutWidget);
MasterServerLineEdit->setText(configini->value("master", "").value<QString>());
GameplayForm->setWidget(8, QFormLayout::FieldRole, MasterServerLineEdit);
DiscordLabel = new QLabel(formLayoutWidget);
DiscordLabel->setText("Discord:");
DiscordLabel->setToolTip("If true, allows Discord's Rich Presence to read data about your game. These are: what server you are in, what character are you playing, and how long have you been playing for.");
GameplayForm->setWidget(9, QFormLayout::LabelRole, DiscordLabel);
DiscordCheckBox = new QCheckBox(formLayoutWidget);
DiscordCheckBox->setChecked(ao_app->is_discord_enabled());
GameplayForm->setWidget(9, QFormLayout::FieldRole, DiscordCheckBox);
// Here we start the callwords tab.
CallwordsTab = new QWidget();
SettingsTabs->addTab(CallwordsTab, "Callwords");
verticalLayoutWidget = new QWidget(CallwordsTab);
verticalLayoutWidget->setGeometry(QRect(10, 10, 361, 211));
CallwordsLayout = new QVBoxLayout(verticalLayoutWidget);
CallwordsLayout->setContentsMargins(0,0,0,0);
CallwordsTextEdit = new QPlainTextEdit(verticalLayoutWidget);
QSizePolicy sizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
sizePolicy.setHorizontalStretch(0);
sizePolicy.setVerticalStretch(0);
sizePolicy.setHeightForWidth(CallwordsTextEdit->sizePolicy().hasHeightForWidth());
CallwordsTextEdit->setSizePolicy(sizePolicy);
// Let's fill the callwords text edit with the already present callwords.
CallwordsTextEdit->document()->clear();
foreach (QString callword, p_ao_app->get_call_words()) {
CallwordsTextEdit->appendPlainText(callword);
}
CallwordsLayout->addWidget(CallwordsTextEdit);
CallwordsExplainLabel = new QLabel(verticalLayoutWidget);
CallwordsExplainLabel->setWordWrap(true);
CallwordsExplainLabel->setText("<html><head/><body>Enter as many callwords as you would like. These are case insensitive. Make sure to leave every callword in its own line!<br>Do not leave a line with a space at the end -- you will be alerted everytime someone uses a space in their messages.</body></html>");
CallwordsLayout->addWidget(CallwordsExplainLabel);
// The audio tab.
AudioTab = new QWidget();
SettingsTabs->addTab(AudioTab, "Audio");
formLayoutWidget_2 = new QWidget(AudioTab);
formLayoutWidget_2->setGeometry(QRect(10, 10, 361, 211));
AudioForm = new QFormLayout(formLayoutWidget_2);
AudioForm->setObjectName(QStringLiteral("AudioForm"));
AudioForm->setLabelAlignment(Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter);
AudioForm->setFormAlignment(Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop);
AudioForm->setContentsMargins(0, 0, 0, 0);
AudioDevideLabel = new QLabel(formLayoutWidget_2);
AudioDevideLabel->setText("Audio device:");
AudioDevideLabel->setToolTip("Allows you to set the theme used ingame. If your theme changes the lobby's look, too, you'll obviously need to reload the lobby somehow for it take effect. Joining a server and leaving it should work.");
AudioForm->setWidget(0, QFormLayout::LabelRole, AudioDevideLabel);
AudioDeviceCombobox = new QComboBox(formLayoutWidget_2);
// Let's fill out the combobox with the available audio devices.
int a = 0;
BASS_DEVICEINFO info;
if (needs_default_audiodev())
{
AudioDeviceCombobox->addItem("Default");
}
for (a = 0; BASS_GetDeviceInfo(a, &info); a++)
{
AudioDeviceCombobox->addItem(info.name);
if (p_ao_app->get_audio_output_device() == info.name)
AudioDeviceCombobox->setCurrentIndex(AudioDeviceCombobox->count()-1);
}
AudioForm->setWidget(0, QFormLayout::FieldRole, AudioDeviceCombobox);
DeviceVolumeDivider = new QFrame(formLayoutWidget_2);
DeviceVolumeDivider->setFrameShape(QFrame::HLine);
DeviceVolumeDivider->setFrameShadow(QFrame::Sunken);
AudioForm->setWidget(1, QFormLayout::FieldRole, DeviceVolumeDivider);
MusicVolumeLabel = new QLabel(formLayoutWidget_2);
MusicVolumeLabel->setText("Music:");
MusicVolumeLabel->setToolTip("Sets the music's default volume.");
AudioForm->setWidget(2, QFormLayout::LabelRole, MusicVolumeLabel);
MusicVolumeSpinbox = new QSpinBox(formLayoutWidget_2);
MusicVolumeSpinbox->setValue(p_ao_app->get_default_music());
MusicVolumeSpinbox->setMaximum(100);
MusicVolumeSpinbox->setSuffix("%");
AudioForm->setWidget(2, QFormLayout::FieldRole, MusicVolumeSpinbox);
SFXVolumeLabel = new QLabel(formLayoutWidget_2);
SFXVolumeLabel->setText("SFX:");
SFXVolumeLabel->setToolTip("Sets the SFX's default volume. Interjections and actual sound effects count as 'SFX'.");
AudioForm->setWidget(3, QFormLayout::LabelRole, SFXVolumeLabel);
SFXVolumeSpinbox = new QSpinBox(formLayoutWidget_2);
SFXVolumeSpinbox->setValue(p_ao_app->get_default_sfx());
SFXVolumeSpinbox->setMaximum(100);
SFXVolumeSpinbox->setSuffix("%");
AudioForm->setWidget(3, QFormLayout::FieldRole, SFXVolumeSpinbox);
BlipsVolumeLabel = new QLabel(formLayoutWidget_2);
BlipsVolumeLabel->setText("Blips:");
BlipsVolumeLabel->setToolTip("Sets the volume of the blips, the talking sound effects.");
AudioForm->setWidget(4, QFormLayout::LabelRole, BlipsVolumeLabel);
BlipsVolumeSpinbox = new QSpinBox(formLayoutWidget_2);
BlipsVolumeSpinbox->setValue(p_ao_app->get_default_blip());
BlipsVolumeSpinbox->setMaximum(100);
BlipsVolumeSpinbox->setSuffix("%");
AudioForm->setWidget(4, QFormLayout::FieldRole, BlipsVolumeSpinbox);
VolumeBlipDivider = new QFrame(formLayoutWidget_2);
VolumeBlipDivider->setFrameShape(QFrame::HLine);
VolumeBlipDivider->setFrameShadow(QFrame::Sunken);
AudioForm->setWidget(5, QFormLayout::FieldRole, VolumeBlipDivider);
BlipRateLabel = new QLabel(formLayoutWidget_2);
BlipRateLabel->setText("Blip rate:");
BlipRateLabel->setToolTip("Sets the delay between playing the blip sounds.");
AudioForm->setWidget(6, QFormLayout::LabelRole, BlipRateLabel);
BlipRateSpinbox = new QSpinBox(formLayoutWidget_2);
BlipRateSpinbox->setValue(p_ao_app->read_blip_rate());
BlipRateSpinbox->setMinimum(1);
AudioForm->setWidget(6, QFormLayout::FieldRole, BlipRateSpinbox);
BlankBlipsLabel = new QLabel(formLayoutWidget_2);
BlankBlipsLabel->setText("Blank blips:");
BlankBlipsLabel->setToolTip("If true, the game will play a blip sound even when a space is 'being said'.");
AudioForm->setWidget(7, QFormLayout::LabelRole, BlankBlipsLabel);
BlankBlipsCheckbox = new QCheckBox(formLayoutWidget_2);
BlankBlipsCheckbox->setChecked(p_ao_app->get_blank_blip());
AudioForm->setWidget(7, QFormLayout::FieldRole, BlankBlipsCheckbox);
// The casing tab!
CasingTab = new QWidget();
SettingsTabs->addTab(CasingTab, "Casing");
formLayoutWidget_3 = new QWidget(CasingTab);
formLayoutWidget_3->setGeometry(QRect(10,10, 361, 211));
CasingForm = new QFormLayout(formLayoutWidget_3);
CasingForm->setObjectName(QStringLiteral("CasingForm"));
CasingForm->setLabelAlignment(Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter);
CasingForm->setFormAlignment(Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop);
CasingForm->setContentsMargins(0, 0, 0, 0);
// -- SERVER SUPPORTS CASING
ServerSupportsCasing = new QLabel(formLayoutWidget_3);
if (ao_app->casing_alerts_enabled)
ServerSupportsCasing->setText("This server supports case alerts.");
else
ServerSupportsCasing->setText("This server does not support case alerts.");
ServerSupportsCasing->setToolTip("Pretty self-explanatory.");
CasingForm->setWidget(0, QFormLayout::FieldRole, ServerSupportsCasing);
// -- CASE ANNOUNCEMENTS
CasingEnabledLabel = new QLabel(formLayoutWidget_3);
CasingEnabledLabel->setText("Casing:");
CasingEnabledLabel->setToolTip("If checked, you will get alerts about case announcements.");
CasingForm->setWidget(1, QFormLayout::LabelRole, CasingEnabledLabel);
CasingEnabledCheckbox = new QCheckBox(formLayoutWidget_3);
CasingEnabledCheckbox->setChecked(ao_app->get_casing_enabled());
CasingForm->setWidget(1, QFormLayout::FieldRole, CasingEnabledCheckbox);
// -- DEFENCE ANNOUNCEMENTS
DefenceLabel = new QLabel(formLayoutWidget_3);
DefenceLabel->setText("Defence:");
DefenceLabel->setToolTip("If checked, you will get alerts about case announcements if a defence spot is open.");
CasingForm->setWidget(2, QFormLayout::LabelRole, DefenceLabel);
DefenceCheckbox = new QCheckBox(formLayoutWidget_3);
DefenceCheckbox->setChecked(ao_app->get_casing_defence_enabled());
CasingForm->setWidget(2, QFormLayout::FieldRole, DefenceCheckbox);
// -- PROSECUTOR ANNOUNCEMENTS
ProsecutorLabel = new QLabel(formLayoutWidget_3);
ProsecutorLabel->setText("Prosecution:");
ProsecutorLabel->setToolTip("If checked, you will get alerts about case announcements if a prosecutor spot is open.");
CasingForm->setWidget(3, QFormLayout::LabelRole, ProsecutorLabel);
ProsecutorCheckbox = new QCheckBox(formLayoutWidget_3);
ProsecutorCheckbox->setChecked(ao_app->get_casing_prosecution_enabled());
CasingForm->setWidget(3, QFormLayout::FieldRole, ProsecutorCheckbox);
// -- JUDGE ANNOUNCEMENTS
JudgeLabel = new QLabel(formLayoutWidget_3);
JudgeLabel->setText("Judge:");
JudgeLabel->setToolTip("If checked, you will get alerts about case announcements if the judge spot is open.");
CasingForm->setWidget(4, QFormLayout::LabelRole, JudgeLabel);
JudgeCheckbox = new QCheckBox(formLayoutWidget_3);
JudgeCheckbox->setChecked(ao_app->get_casing_judge_enabled());
CasingForm->setWidget(4, QFormLayout::FieldRole, JudgeCheckbox);
// -- JUROR ANNOUNCEMENTS
JurorLabel = new QLabel(formLayoutWidget_3);
JurorLabel->setText("Juror:");
JurorLabel->setToolTip("If checked, you will get alerts about case announcements if a juror spot is open.");
CasingForm->setWidget(5, QFormLayout::LabelRole, JurorLabel);
JurorCheckbox = new QCheckBox(formLayoutWidget_3);
JurorCheckbox->setChecked(ao_app->get_casing_juror_enabled());
CasingForm->setWidget(5, QFormLayout::FieldRole, JurorCheckbox);
// -- STENO ANNOUNCEMENTS
StenographerLabel = new QLabel(formLayoutWidget_3);
StenographerLabel->setText("Stenographer:");
StenographerLabel->setToolTip("If checked, you will get alerts about case announcements if a stenographer spot is open.");
CasingForm->setWidget(6, QFormLayout::LabelRole, StenographerLabel);
StenographerCheckbox = new QCheckBox(formLayoutWidget_3);
StenographerCheckbox->setChecked(ao_app->get_casing_steno_enabled());
CasingForm->setWidget(6, QFormLayout::FieldRole, StenographerCheckbox);
// -- CM ANNOUNCEMENTS
CMLabel = new QLabel(formLayoutWidget_3);
CMLabel->setText("CM:");
CMLabel->setToolTip("If checked, you will appear amongst the potential CMs on the server.");
CasingForm->setWidget(7, QFormLayout::LabelRole, CMLabel);
CMCheckbox = new QCheckBox(formLayoutWidget_3);
CMCheckbox->setChecked(ao_app->get_casing_cm_enabled());
CasingForm->setWidget(7, QFormLayout::FieldRole, CMCheckbox);
// -- CM CASES ANNOUNCEMENTS
CMCasesLabel = new QLabel(formLayoutWidget_3);
CMCasesLabel->setText("Hosting cases:");
CMCasesLabel->setToolTip("If you're a CM, enter what cases are you willing to host.");
CasingForm->setWidget(8, QFormLayout::LabelRole, CMCasesLabel);
CMCasesLineEdit = new QLineEdit(formLayoutWidget_3);
CMCasesLineEdit->setText(ao_app->get_casing_can_host_cases());
CasingForm->setWidget(8, QFormLayout::FieldRole, CMCasesLineEdit);
// When we're done, we should continue the updates!
setUpdatesEnabled(true);
}
void AOOptionsDialog::save_pressed()
{
// Save everything into the config.ini.
QSettings* configini = ao_app->configini;
configini->setValue("theme", ThemeCombobox->currentText());
configini->setValue("log_goes_downwards", DownwardCheckbox->isChecked());
configini->setValue("log_maximum", LengthSpinbox->value());
configini->setValue("default_username", UsernameLineEdit->text());
configini->setValue("show_custom_shownames", ShownameCheckbox->isChecked());
configini->setValue("master", MasterServerLineEdit->text());
configini->setValue("discord", DiscordCheckBox->isChecked());
QFile* callwordsini = new QFile(ao_app->get_base_path() + "callwords.ini");
if (!callwordsini->open(QIODevice::WriteOnly | QIODevice::Truncate | QIODevice::Text))
{
// Nevermind!
}
else
{
QTextStream out(callwordsini);
out << CallwordsTextEdit->toPlainText();
callwordsini->close();
}
configini->setValue("default_audio_device", AudioDeviceCombobox->currentText());
configini->setValue("default_music", MusicVolumeSpinbox->value());
configini->setValue("default_sfx", SFXVolumeSpinbox->value());
configini->setValue("default_blip", BlipsVolumeSpinbox->value());
configini->setValue("blip_rate", BlipRateSpinbox->value());
configini->setValue("blank_blip", BlankBlipsCheckbox->isChecked());
configini->setValue("casing_enabled", CasingEnabledCheckbox->isChecked());
configini->setValue("casing_defence_enabled", DefenceCheckbox->isChecked());
configini->setValue("casing_prosecution_enabled", ProsecutorCheckbox->isChecked());
configini->setValue("casing_judge_enabled", JudgeCheckbox->isChecked());
configini->setValue("casing_juror_enabled", JurorCheckbox->isChecked());
configini->setValue("casing_steno_enabled", StenographerCheckbox->isChecked());
configini->setValue("casing_cm_enabled", CMCheckbox->isChecked());
configini->setValue("casing_can_host_casees", CMCasesLineEdit->text());
callwordsini->close();
done(0);
}
void AOOptionsDialog::discard_pressed()
{
done(0);
}
#if (defined (_WIN32) || defined (_WIN64))
bool AOOptionsDialog::needs_default_audiodev()
{
return true;
}
#elif (defined (LINUX) || defined (__linux__))
bool AOOptionsDialog::needs_default_audiodev()
{
return false;
}
#else
#error This operating system is not supported.
#endif

115
aooptionsdialog.h Normal file
View File

@ -0,0 +1,115 @@
#ifndef AOOPTIONSDIALOG_H
#define AOOPTIONSDIALOG_H
#include "aoapplication.h"
#include "bass.h"
#include <QtCore/QVariant>
#include <QtWidgets/QApplication>
#include <QtWidgets/QCheckBox>
#include <QtWidgets/QComboBox>
#include <QtWidgets/QDialog>
#include <QtWidgets/QDialogButtonBox>
#include <QtWidgets/QFormLayout>
#include <QtWidgets/QFrame>
#include <QtWidgets/QLabel>
#include <QtWidgets/QLineEdit>
#include <QtWidgets/QPlainTextEdit>
#include <QtWidgets/QSpinBox>
#include <QtWidgets/QTabWidget>
#include <QtWidgets/QVBoxLayout>
#include <QtWidgets/QWidget>
#include <QDirIterator>
#include <QTextStream>
class AOOptionsDialog: public QDialog
{
Q_OBJECT
public:
explicit AOOptionsDialog(QWidget *parent = nullptr, AOApplication *p_ao_app = nullptr);
private:
AOApplication *ao_app;
QVBoxLayout *verticalLayout;
QTabWidget *SettingsTabs;
QWidget *GameplayTab;
QWidget *formLayoutWidget;
QFormLayout *GameplayForm;
QLabel *ThemeLabel;
QComboBox *ThemeCombobox;
QFrame *ThemeLogDivider;
QLabel *DownwardsLabel;
QCheckBox *DownwardCheckbox;
QLabel *LengthLabel;
QSpinBox *LengthSpinbox;
QFrame *LogNamesDivider;
QLineEdit *UsernameLineEdit;
QLabel *UsernameLabel;
QLabel *ShownameLabel;
QCheckBox *ShownameCheckbox;
QFrame *NetDivider;
QLabel *MasterServerLabel;
QLineEdit *MasterServerLineEdit;
QLabel *DiscordLabel;
QCheckBox *DiscordCheckBox;
QWidget *CallwordsTab;
QWidget *verticalLayoutWidget;
QVBoxLayout *CallwordsLayout;
QPlainTextEdit *CallwordsTextEdit;
QLabel *CallwordsExplainLabel;
QCheckBox *CharacterCallwordsCheckbox;
QWidget *AudioTab;
QWidget *formLayoutWidget_2;
QFormLayout *AudioForm;
QLabel *AudioDevideLabel;
QComboBox *AudioDeviceCombobox;
QFrame *DeviceVolumeDivider;
QSpinBox *MusicVolumeSpinbox;
QLabel *MusicVolumeLabel;
QSpinBox *SFXVolumeSpinbox;
QSpinBox *BlipsVolumeSpinbox;
QLabel *SFXVolumeLabel;
QLabel *BlipsVolumeLabel;
QFrame *VolumeBlipDivider;
QSpinBox *BlipRateSpinbox;
QLabel *BlipRateLabel;
QCheckBox *BlankBlipsCheckbox;
QLabel *BlankBlipsLabel;
QDialogButtonBox *SettingsButtons;
QWidget *CasingTab;
QWidget *formLayoutWidget_3;
QFormLayout *CasingForm;
QLabel *ServerSupportsCasing;
QLabel *CasingEnabledLabel;
QCheckBox *CasingEnabledCheckbox;
QLabel *DefenceLabel;
QCheckBox *DefenceCheckbox;
QLabel *ProsecutorLabel;
QCheckBox *ProsecutorCheckbox;
QLabel *JudgeLabel;
QCheckBox *JudgeCheckbox;
QLabel *JurorLabel;
QCheckBox *JurorCheckbox;
QLabel *StenographerLabel;
QCheckBox *StenographerCheckbox;
QLabel *CMLabel;
QCheckBox *CMCheckbox;
QLabel *CMCasesLabel;
QLineEdit *CMCasesLineEdit;
bool needs_default_audiodev();
signals:
public slots:
void save_pressed();
void discard_pressed();
};
#endif // AOOPTIONSDIALOG_H

View File

@ -6,6 +6,7 @@ AOScene::AOScene(QWidget *parent, AOApplication *p_ao_app) : QLabel(parent)
{
m_parent = parent;
ao_app = p_ao_app;
m_movie = new QMovie(this);
}
void AOScene::set_image(QString p_image)
@ -15,18 +16,31 @@ void AOScene::set_image(QString p_image)
QString default_path = ao_app->get_default_background_path() + p_image;
QPixmap background(background_path);
QPixmap animated_background(animated_background_path);
QPixmap default_bg(default_path);
int w = this->width();
int h = this->height();
if (file_exists(animated_background_path))
this->setPixmap(animated_background.scaled(w, h));
this->clear();
this->setMovie(nullptr);
m_movie->stop();
m_movie->setFileName(animated_background_path);
m_movie->setScaledSize(QSize(w, h));
if (m_movie->isValid())
{
this->setMovie(m_movie);
m_movie->start();
}
else if (file_exists(background_path))
{
this->setPixmap(background.scaled(w, h));
}
else
{
this->setPixmap(default_bg.scaled(w, h));
}
}
void AOScene::set_legacy_desk(QString p_image)

View File

@ -3,6 +3,7 @@
#include <QLabel>
#include <QDebug>
#include <QMovie>
class Courtroom;
class AOApplication;
@ -18,6 +19,7 @@ public:
private:
QWidget *m_parent;
QMovie *m_movie;
AOApplication *ao_app;
};

View File

@ -1,41 +1,49 @@
#include "aosfxplayer.h"
#include "file_functions.h"
AOSfxPlayer::AOSfxPlayer(QWidget *parent, AOApplication *p_ao_app)
{
m_sfxplayer = new QSoundEffect;
m_parent = parent;
ao_app = p_ao_app;
}
AOSfxPlayer::~AOSfxPlayer()
void AOSfxPlayer::play(QString p_sfx, QString p_char, QString shout)
{
m_sfxplayer->stop();
m_sfxplayer->deleteLater();
}
void AOSfxPlayer::play(QString p_sfx, QString p_char)
{
m_sfxplayer->stop();
p_sfx = p_sfx.toLower();
QString f_path;
if (p_char != "")
f_path = ao_app->get_character_path(p_char) + p_sfx;
else
f_path = ao_app->get_sounds_path() + p_sfx;
m_sfxplayer->setSource(QUrl::fromLocalFile(f_path));
QString misc_path = "";
QString char_path = "";
QString sound_path = ao_app->get_sounds_path() + p_sfx;
if (shout != "")
misc_path = ao_app->get_base_path() + "misc/" + shout + "/" + p_sfx;
if (p_char != "")
char_path = ao_app->get_character_path(p_char) + p_sfx;
QString f_path;
if (file_exists(char_path))
f_path = char_path;
else if (file_exists(misc_path))
f_path = misc_path;
else
f_path = sound_path;
set_volume(m_volume);
m_sfxplayer->play();
if (ao_app->get_audio_output_device() != "Default")
BASS_ChannelSetDevice(m_stream, BASS_GetDevice());
BASS_ChannelPlay(m_stream, false);
}
void AOSfxPlayer::stop()
{
m_sfxplayer->stop();
BASS_ChannelStop(m_stream);
}
void AOSfxPlayer::set_volume(int p_value)
{
m_volume = p_value;
m_sfxplayer->setVolume(p_value / 100.0);
float volume = p_value / 100.0f;
BASS_ChannelSetAttribute(m_stream, BASS_ATTRIB_VOL, volume);
}

View File

@ -1,29 +1,28 @@
#ifndef AOSFXPLAYER_H
#define AOSFXPLAYER_H
#include "bass.h"
#include "aoapplication.h"
#include <QWidget>
#include <string.h>
#include <QDebug>
#include <QSoundEffect>
class AOSfxPlayer
{
public:
AOSfxPlayer(QWidget *parent, AOApplication *p_ao_app);
~AOSfxPlayer();
void play(QString p_sfx, QString p_char = "");
void play(QString p_sfx, QString p_char = "", QString shout = "");
void stop();
void set_volume(int p_volume);
private:
QWidget *m_parent;
AOApplication *ao_app;
QSoundEffect *m_sfxplayer;
int m_volume = 0;
HSTREAM m_stream;
};
#endif // AOSFXPLAYER_H

View File

@ -5,7 +5,7 @@ AOTextArea::AOTextArea(QWidget *p_parent) : QTextBrowser(p_parent)
}
void AOTextArea::append_chatmessage(QString p_name, QString p_message)
void AOTextArea::append_chatmessage(QString p_name, QString p_message, QString p_colour)
{
const QTextCursor old_cursor = this->textCursor();
const int old_scrollbar_value = this->verticalScrollBar()->value();
@ -14,7 +14,7 @@ void AOTextArea::append_chatmessage(QString p_name, QString p_message)
this->moveCursor(QTextCursor::End);
this->append("");
this->insertHtml("<b>" + p_name.toHtmlEscaped() + "</b>:&nbsp;");
this->insertHtml("<b><font color=" + p_colour + ">" + p_name.toHtmlEscaped() + "</font></b>:&nbsp;");
//cheap workarounds ahoy
p_message += " ";

View File

@ -12,7 +12,7 @@ class AOTextArea : public QTextBrowser
public:
AOTextArea(QWidget *p_parent = nullptr);
void append_chatmessage(QString p_name, QString p_message);
void append_chatmessage(QString p_name, QString p_message, QString p_colour);
void append_error(QString p_message);
private:

View File

@ -1,11 +1,10 @@
#include "courtroom.h"
#include "lobby.h"
#include "file_functions.h"
#include "debug_functions.h"
#include "hardware_functions.h"
#include <QDebug>
void Courtroom::construct_char_select()
{
ui_char_select_background = new AOImage(this, ao_app);
@ -19,6 +18,7 @@ void Courtroom::construct_char_select()
ui_back_to_lobby = new AOButton(ui_char_select_background, ao_app);
ui_char_password = new QLineEdit(ui_char_select_background);
ui_char_password->setPlaceholderText("Password");
ui_char_select_left = new AOButton(ui_char_select_background, ao_app);
ui_char_select_right = new AOButton(ui_char_select_background, ao_app);
@ -26,42 +26,24 @@ void Courtroom::construct_char_select()
ui_spectator = new AOButton(ui_char_select_background, ao_app);
ui_spectator->setText("Spectator");
QPoint f_spacing = ao_app->get_button_spacing("char_button_spacing", "courtroom_design.ini");
ui_char_search = new QLineEdit(ui_char_select_background);
ui_char_search->setPlaceholderText("Search");
ui_char_search->setFocus();
set_size_and_pos(ui_char_search, "char_search");
const int button_width = 60;
int x_spacing = f_spacing.x();
int x_mod_count = 0;
ui_char_passworded = new QCheckBox(ui_char_select_background);
ui_char_passworded->setText("Passworded");
set_size_and_pos(ui_char_passworded, "char_passworded");
const int button_height = 60;
int y_spacing = f_spacing.y();
int y_mod_count = 0;
ui_char_taken = new QCheckBox(ui_char_select_background);
ui_char_taken->setText("Taken");
set_size_and_pos(ui_char_taken, "char_taken");
ui_char_taken->setChecked(true);
ui_char_passworded->setChecked(true);
set_size_and_pos(ui_char_buttons, "char_buttons");
char_columns = ((ui_char_buttons->width() - button_width) / (x_spacing + button_width)) + 1;
char_rows = ((ui_char_buttons->height() - button_height) / (y_spacing + button_height)) + 1;
max_chars_on_page = char_columns * char_rows;
for (int n = 0 ; n < max_chars_on_page ; ++n)
{
int x_pos = (button_width + x_spacing) * x_mod_count;
int y_pos = (button_height + y_spacing) * y_mod_count;
ui_char_button_list.append(new AOCharButton(ui_char_buttons, ao_app, x_pos, y_pos));
connect(ui_char_button_list.at(n), SIGNAL(clicked()), char_button_mapper, SLOT(map())) ;
char_button_mapper->setMapping (ui_char_button_list.at(n), n) ;
++x_mod_count;
if (x_mod_count == char_columns)
{
++y_mod_count;
x_mod_count = 0;
}
}
connect (char_button_mapper, SIGNAL(mapped(int)), this, SLOT(char_clicked(int)));
connect(ui_back_to_lobby, SIGNAL(clicked()), this, SLOT(on_back_to_lobby_clicked()));
@ -69,6 +51,10 @@ void Courtroom::construct_char_select()
connect(ui_char_select_right, SIGNAL(clicked()), this, SLOT(on_char_select_right_clicked()));
connect(ui_spectator, SIGNAL(clicked()), this, SLOT(on_spectator_clicked()));
connect(ui_char_search, SIGNAL(textEdited(const QString&)), this, SLOT(on_char_search_changed(const QString&)));
connect(ui_char_passworded, SIGNAL(stateChanged(int)), this, SLOT(on_char_passworded_clicked(int)));
connect(ui_char_taken, SIGNAL(stateChanged(int)), this, SLOT(on_char_taken_clicked(int)));
}
void Courtroom::set_char_select()
@ -87,6 +73,10 @@ void Courtroom::set_char_select()
ui_char_select_background->resize(f_charselect.width, f_charselect.height);
ui_char_select_background->set_image("charselect_background.png");
filter_character_list();
ui_char_search->setFocus();
}
void Courtroom::set_char_select_page()
@ -97,19 +87,23 @@ void Courtroom::set_char_select_page()
ui_char_select_right->hide();
for (AOCharButton *i_button : ui_char_button_list)
{
i_button->reset();
i_button->hide();
i_button->move(0,0);
}
int total_pages = char_list.size() / max_chars_on_page;
int total_pages = ui_char_button_list_filtered.size() / max_chars_on_page;
int chars_on_page = 0;
if (char_list.size() % max_chars_on_page != 0)
if (ui_char_button_list_filtered.size() % max_chars_on_page != 0)
{
++total_pages;
//i. e. not on the last page
if (total_pages > current_char_page + 1)
chars_on_page = max_chars_on_page;
else
chars_on_page = char_list.size() % max_chars_on_page;
chars_on_page = ui_char_button_list_filtered.size() % max_chars_on_page;
}
else
@ -121,26 +115,12 @@ void Courtroom::set_char_select_page()
if (current_char_page > 0)
ui_char_select_left->show();
for (int n_button = 0 ; n_button < chars_on_page ; ++n_button)
{
int n_real_char = n_button + current_char_page * max_chars_on_page;
AOCharButton *f_button = ui_char_button_list.at(n_button);
f_button->reset();
f_button->set_image(char_list.at(n_real_char).name);
f_button->show();
if (char_list.at(n_real_char).taken)
f_button->set_taken();
}
put_button_in_place(current_char_page * max_chars_on_page, chars_on_page);
}
void Courtroom::char_clicked(int n_char)
{
int n_real_char = n_char + current_char_page * max_chars_on_page;
QString char_ini_path = ao_app->get_character_path(char_list.at(n_real_char).name) + "char.ini";
QString char_ini_path = ao_app->get_character_path(char_list.at(n_char).name) + "char.ini";
qDebug() << "char_ini_path" << char_ini_path;
if (!file_exists(char_ini_path))
@ -150,13 +130,134 @@ void Courtroom::char_clicked(int n_char)
return;
}
if (n_real_char == m_cid)
if (n_char == m_cid)
{
enter_courtroom(m_cid);
}
else
{
ao_app->send_server_packet(new AOPacket("CC#" + QString::number(ao_app->s_pv) + "#" + QString::number(n_real_char) + "#" + get_hdid() + "#%"));
ao_app->send_server_packet(new AOPacket("CC#" + QString::number(ao_app->s_pv) + "#" + QString::number(n_char) + "#" + get_hdid() + "#%"));
}
ui_ic_chat_name->setPlaceholderText(char_list.at(n_char).name);
}
void Courtroom::put_button_in_place(int starting, int chars_on_this_page)
{
if (ui_char_button_list_filtered.size() == 0)
return;
QPoint f_spacing = ao_app->get_button_spacing("char_button_spacing", "courtroom_design.ini");
int x_spacing = f_spacing.x();
int x_mod_count = 0;
int y_spacing = f_spacing.y();
int y_mod_count = 0;
char_columns = ((ui_char_buttons->width() - button_width) / (x_spacing + button_width)) + 1;
char_rows = ((ui_char_buttons->height() - button_height) / (y_spacing + button_height)) + 1;
max_chars_on_page = char_columns * char_rows;
int startout = starting;
for (int n = starting ; n < startout+chars_on_this_page ; ++n)
{
int x_pos = (button_width + x_spacing) * x_mod_count;
int y_pos = (button_height + y_spacing) * y_mod_count;
ui_char_button_list_filtered.at(n)->move(x_pos, y_pos);
ui_char_button_list_filtered.at(n)->show();
ui_char_button_list_filtered.at(n)->set_taken();
++x_mod_count;
if (x_mod_count == char_columns)
{
++y_mod_count;
x_mod_count = 0;
}
}
}
void Courtroom::character_loading_finished()
{
// Zeroeth, we'll clear any leftover characters from previous server visits.
ao_app->generated_chars = 0;
if (ui_char_button_list.size() > 0)
{
foreach (AOCharButton* item, ui_char_button_list) {
delete item;
}
ui_char_button_list.clear();
}
// First, we'll make all the character buttons in the very beginning.
// We also hide them all, so they can't be accidentally clicked.
// Later on, we'll be revealing buttons as we need them.
for (int n = 0; n < char_list.size(); n++)
{
AOCharButton* character = new AOCharButton(ui_char_buttons, ao_app, 0, 0, char_list.at(n).taken);
character->reset();
character->hide();
character->set_image(char_list.at(n).name);
ui_char_button_list.append(character);
connect(character, SIGNAL(clicked()), char_button_mapper, SLOT(map()));
char_button_mapper->setMapping(character, ui_char_button_list.size() - 1);
// This part here serves as a way of showing to the player that the game is still running, it is
// just loading the pictures of the characters.
if (ao_app->lobby_constructed)
{
ao_app->generated_chars++;
int total_loading_size = ao_app->char_list_size * 2 + ao_app->evidence_list_size + ao_app->music_list_size;
int loading_value = int(((ao_app->loaded_chars + ao_app->generated_chars + ao_app->loaded_music + ao_app->loaded_evidence) / static_cast<double>(total_loading_size)) * 100);
ao_app->w_lobby->set_loading_value(loading_value);
ao_app->w_lobby->set_loading_text("Generating chars:\n" + QString::number(ao_app->generated_chars) + "/" + QString::number(ao_app->char_list_size));
}
}
filter_character_list();
}
void Courtroom::filter_character_list()
{
ui_char_button_list_filtered.clear();
for (int i = 0; i < char_list.size(); i++)
{
AOCharButton* current_char = ui_char_button_list.at(i);
// It seems passwording characters is unimplemented yet?
// Until then, this will stay here, I suppose.
//if (ui_char_passworded->isChecked() && character_is_passworded??)
// continue;
if (!ui_char_taken->isChecked() && char_list.at(i).taken)
continue;
if (!char_list.at(i).name.contains(ui_char_search->text(), Qt::CaseInsensitive))
continue;
ui_char_button_list_filtered.append(current_char);
}
current_char_page = 0;
set_char_select_page();
}
void Courtroom::on_char_search_changed(const QString& newtext)
{
filter_character_list();
}
void Courtroom::on_char_passworded_clicked(int newstate)
{
filter_character_list();
}
void Courtroom::on_char_taken_clicked(int newstate)
{
filter_character_list();
}

76
chatlogpiece.cpp Normal file
View File

@ -0,0 +1,76 @@
#include "chatlogpiece.h"
chatlogpiece::chatlogpiece()
{
name = "UNKNOWN";
showname = "UNKNOWN";
message = "UNKNOWN";
is_song = false;
datetime = QDateTime::currentDateTime().toUTC();
}
chatlogpiece::chatlogpiece(QString p_name, QString p_showname, QString p_message, bool p_song)
{
name = p_name;
showname = p_showname;
message = p_message;
is_song = p_song;
datetime = QDateTime::currentDateTime().toUTC();
}
chatlogpiece::chatlogpiece(QString p_name, QString p_showname, QString p_message, bool p_song, QDateTime p_datetime)
{
name = p_name;
showname = p_showname;
message = p_message;
is_song = p_song;
datetime = p_datetime.toUTC();
}
QString chatlogpiece::get_name()
{
return name;
}
QString chatlogpiece::get_showname()
{
return showname;
}
QString chatlogpiece::get_message()
{
return message;
}
QDateTime chatlogpiece::get_datetime()
{
return datetime;
}
bool chatlogpiece::get_is_song()
{
return is_song;
}
QString chatlogpiece::get_datetime_as_string()
{
return datetime.toString();
}
QString chatlogpiece::get_full()
{
QString full = "[";
full.append(get_datetime_as_string());
full.append(" UTC] ");
full.append(get_showname());
full.append(" (");
full.append(get_name());
full.append(")");
if (is_song)
full.append(" has played a song: ");
full.append(get_message());
return full;
}

31
chatlogpiece.h Normal file
View File

@ -0,0 +1,31 @@
#ifndef CHATLOGPIECE_H
#define CHATLOGPIECE_H
#include <QString>
#include <QDateTime>
class chatlogpiece
{
public:
chatlogpiece();
chatlogpiece(QString p_name, QString p_showname, QString p_message, bool p_song);
chatlogpiece(QString p_name, QString p_showname, QString p_message, bool p_song, QDateTime p_datetime);
QString get_name();
QString get_showname();
QString get_message();
bool get_is_song();
QDateTime get_datetime();
QString get_datetime_as_string();
QString get_full();
private:
QString name;
QString showname;
QString message;
QDateTime datetime;
bool is_song;
};
#endif // CHATLOGPIECE_H

File diff suppressed because it is too large Load Diff

View File

@ -24,13 +24,8 @@
#include "file_functions.h"
#include "datatypes.h"
#include "debug_functions.h"
#include "chatlogpiece.h"
#include <QDebug>
#include <QScrollBar>
#include <QRegExp>
#include <QBrush>
#include <QTextCharFormat>
#include <QFont>
#include <QMainWindow>
#include <QLineEdit>
#include <QPlainTextEdit>
@ -43,6 +38,18 @@
#include <QSignalMapper>
#include <QMap>
#include <QTextBrowser>
#include <QSpinBox>
#include <QDebug>
#include <QScrollBar>
#include <QRegExp>
#include <QBrush>
#include <QTextCharFormat>
#include <QFont>
#include <QInputDialog>
#include <QFileDialog>
#include <stack>
class AOApplication;
@ -55,6 +62,49 @@ public:
void append_char(char_type p_char){char_list.append(p_char);}
void append_evidence(evi_type p_evi){evidence_list.append(p_evi);}
void append_music(QString f_music){music_list.append(f_music);}
void append_area(QString f_area){area_list.append(f_area);}
void fix_last_area()
{
QString malplaced = area_list.last();
area_list.removeLast();
append_music(malplaced);
}
void arup_append(int players, QString status, QString cm, QString locked)
{
arup_players.append(players);
arup_statuses.append(status);
arup_cms.append(cm);
arup_locks.append(locked);
}
void arup_modify(int type, int place, QString value)
{
if (type == 0)
{
if (arup_players.size() > place)
arup_players[place] = value.toInt();
}
else if (type == 1)
{
if (arup_statuses.size() > place)
arup_statuses[place] = value;
}
else if (type == 2)
{
if (arup_cms.size() > place)
arup_cms[place] = value;
}
else if (type == 3)
{
if (arup_locks.size() > place)
arup_locks[place] = value;
}
list_areas();
}
void character_loading_finished();
//sets position of widgets based on theme ini files
void set_widgets();
@ -83,12 +133,18 @@ public:
//sets the local mute list based on characters available on the server
void set_mute_list();
// Sets the local pair list based on the characters available on the server.
void set_pair_list();
//sets desk and bg based on pos in chatmessage
void set_scene();
//sets text color based on text color in chatmessage
void set_text_color();
// And gets the colour, too!
QColor get_text_color(QString color);
//takes in serverD-formatted IP list as prints a converted version to server OOC
//admittedly poorly named
void set_ip_list(QString p_list);
@ -113,10 +169,11 @@ public:
//helper function that populates ui_music_list with the contents of music_list
void list_music();
void list_areas();
//these are for OOC chat
void append_ms_chatmessage(QString f_name, QString f_message);
void append_server_chatmessage(QString p_name, QString p_message);
void append_server_chatmessage(QString p_name, QString p_message, QString p_colour);
//these functions handle chatmessages sequentially.
//The process itself is very convoluted and merits separate documentation
@ -130,19 +187,25 @@ public:
// or the user isn't already scrolled to the top
void append_ic_text(QString p_text, QString p_name = "");
// This is essentially the same as the above, but specifically for song changes.
void append_ic_songchange(QString p_songname, QString p_name = "");
//prints who played the song to IC chat and plays said song(if found on local filesystem)
//takes in a list where the first element is the song name and the second is the char id of who played it
void handle_song(QStringList *p_contents);
void play_preanim();
void play_noninterrupting_preanim();
//plays the witness testimony or cross examination animation based on argument
void handle_wtce(QString p_wtce);
void handle_wtce(QString p_wtce, int variant);
//sets the hp bar of defense(p_bar 1) or pro(p_bar 2)
//state is an number between 0 and 10 inclusive
void set_hp_bar(int p_bar, int p_state);
void announce_case(QString title, bool def, bool pro, bool jud, bool jur, bool steno);
void check_connection_received();
~Courtroom();
@ -159,18 +222,67 @@ private:
int m_viewport_width = 256;
int m_viewport_height = 192;
bool first_message_sent = false;
int maximumMessages = 0;
// This is for inline message-colouring.
enum INLINE_COLOURS {
INLINE_BLUE,
INLINE_GREEN,
INLINE_ORANGE,
INLINE_GREY
};
// A stack of inline colours.
std::stack<INLINE_COLOURS> inline_colour_stack;
bool next_character_is_not_special = false; // If true, write the
// next character as it is.
bool message_is_centered = false;
int current_display_speed = 3;
int message_display_speed[7] = {30, 40, 50, 60, 75, 100, 120};
// This is for checking if the character should start talking again
// when an inline blue text ends.
bool entire_message_is_blue = false;
// And this is the inline 'talking checker'. Counts how 'deep' we are
// in inline blues.
int inline_blue_depth = 0;
// The character ID of the character this user wants to appear alongside with.
int other_charid = -1;
// The offset this user has given if they want to appear alongside someone.
int offset_with_pair = 0;
QVector<char_type> char_list;
QVector<evi_type> evidence_list;
QVector<QString> music_list;
QVector<QString> area_list;
QVector<int> arup_players;
QVector<QString> arup_statuses;
QVector<QString> arup_cms;
QVector<QString> arup_locks;
QSignalMapper *char_button_mapper;
QVector<chatlogpiece> ic_chatlog_history;
// These map music row items and area row items to their actual IDs.
QVector<int> music_row_to_number;
QVector<int> area_row_to_number;
//triggers ping_server() every 60 seconds
QTimer *keepalive_timer;
//determines how fast messages tick onto screen
QTimer *chat_tick_timer;
int chat_tick_interval = 60;
//int chat_tick_interval = 60;
//which tick position(character in chat message) we are at
int tick_pos = 0;
//used to determine how often blips sound
@ -180,6 +292,12 @@ private:
bool rainbow_appended = false;
bool blank_blip = false;
// Used for getting the current maximum blocks allowed in the IC chatlog.
int log_maximum_blocks = 0;
// True, if the log should go downwards.
bool log_goes_downwards = false;
//delay before chat messages starts ticking
QTimer *text_delay_timer;
@ -197,7 +315,7 @@ private:
//every time point in char.inis times this equals the final time
const int time_mod = 40;
static const int chatmessage_size = 15;
static const int chatmessage_size = 23;
QString m_chatmessage[chatmessage_size];
bool chatmessage_is_empty = false;
@ -218,7 +336,7 @@ private:
bool is_muted = false;
//state of animation, 0 = objecting, 1 = preanim, 2 = talking, 3 = idle
//state of animation, 0 = objecting, 1 = preanim, 2 = talking, 3 = idle, 4 = noniterrupting preanim
int anim_state = 3;
//state of text ticking, 0 = not yet ticking, 1 = ticking in progress, 2 = ticking done
@ -242,6 +360,9 @@ private:
int char_rows = 9;
int max_chars_on_page = 90;
const int button_width = 60;
const int button_height = 60;
int current_emote_page = 0;
int current_emote = 0;
int emote_columns = 5;
@ -277,6 +398,7 @@ private:
AOScene *ui_vp_background;
AOMovie *ui_vp_speedlines;
AOCharMovie *ui_vp_player_char;
AOCharMovie *ui_vp_sideplayer_char;
AOScene *ui_vp_desk;
AOScene *ui_vp_legacy_desk;
AOEvidenceDisplay *ui_vp_evidence_display;
@ -297,7 +419,12 @@ private:
QListWidget *ui_area_list;
QListWidget *ui_music_list;
AOButton *ui_pair_button;
QListWidget *ui_pair_list;
QSpinBox *ui_pair_offset_spinbox;
QLineEdit *ui_ic_chat_message;
QLineEdit *ui_ic_chat_name;
QLineEdit *ui_ooc_chat_message;
QLineEdit *ui_ooc_chat_name;
@ -328,14 +455,23 @@ private:
AOButton *ui_witness_testimony;
AOButton *ui_cross_examination;
AOButton *ui_guilty;
AOButton *ui_not_guilty;
AOButton *ui_change_character;
AOButton *ui_reload_theme;
AOButton *ui_call_mod;
AOButton *ui_settings;
AOButton *ui_announce_casing;
AOButton *ui_switch_area_music;
QCheckBox *ui_pre;
QCheckBox *ui_flip;
QCheckBox *ui_guard;
QCheckBox *ui_casing;
QCheckBox *ui_pre_non_interrupt;
QCheckBox *ui_showname_enable;
AOButton *ui_custom_objection;
AOButton *ui_realization;
@ -355,6 +491,9 @@ private:
AOImage *ui_muted;
QSpinBox *ui_log_limit_spinbox;
QLabel *ui_log_limit_label;
AOButton *ui_evidence_button;
AOImage *ui_evidence;
AOLineEdit *ui_evidence_name;
@ -376,6 +515,7 @@ private:
QWidget *ui_char_buttons;
QVector<AOCharButton*> ui_char_button_list;
QVector<AOCharButton*> ui_char_button_list_filtered;
AOImage *ui_selector;
AOButton *ui_back_to_lobby;
@ -387,9 +527,15 @@ private:
AOButton *ui_spectator;
QLineEdit *ui_char_search;
QCheckBox *ui_char_passworded;
QCheckBox *ui_char_taken;
void construct_char_select();
void set_char_select();
void set_char_select_page();
void put_button_in_place(int starting, int chars_on_this_page);
void filter_character_list();
void construct_emotes();
void set_emote_page();
@ -398,8 +544,6 @@ private:
void construct_evidence();
void set_evidence_page();
public slots:
void objection_done();
void preanim_done();
@ -411,6 +555,8 @@ public slots:
void mod_called(QString p_ip);
void case_called(QString msg, bool def, bool pro, bool jud, bool jur, bool steno);
private slots:
void start_chat_ticking();
void play_sfx();
@ -418,6 +564,7 @@ private slots:
void chat_tick();
void on_mute_list_clicked(QModelIndex p_index);
void on_pair_list_clicked(QModelIndex p_index);
void on_chat_return_pressed();
@ -425,6 +572,7 @@ private slots:
void on_music_search_edited(QString p_text);
void on_music_list_double_clicked(QModelIndex p_model);
void on_area_list_double_clicked(QModelIndex p_model);
void select_emote(int p_id);
@ -456,6 +604,7 @@ private slots:
void on_realization_clicked();
void on_mute_clicked();
void on_pair_clicked();
void on_defense_minus_clicked();
void on_defense_plus_clicked();
@ -468,19 +617,28 @@ private slots:
void on_sfx_slider_moved(int p_value);
void on_blip_slider_moved(int p_value);
void on_log_limit_changed(int value);
void on_pair_offset_changed(int value);
void on_ooc_toggle_clicked();
void on_witness_testimony_clicked();
void on_cross_examination_clicked();
void on_not_guilty_clicked();
void on_guilty_clicked();
void on_change_character_clicked();
void on_reload_theme_clicked();
void on_call_mod_clicked();
void on_settings_clicked();
void on_announce_casing_clicked();
void on_pre_clicked();
void on_flip_clicked();
void on_guard_clicked();
void on_showname_enable_clicked();
void on_evidence_button_clicked();
void on_evidence_delete_clicked();
@ -490,12 +648,21 @@ private slots:
void on_char_select_left_clicked();
void on_char_select_right_clicked();
void on_char_search_changed(const QString& newtext);
void on_char_taken_clicked(int newstate);
void on_char_passworded_clicked(int newstate);
void on_spectator_clicked();
void char_clicked(int n_char);
void on_switch_area_music_clicked();
void on_casing_clicked();
void ping_server();
void load_bass_opus_plugin();
};
#endif // COURTROOM_H

View File

@ -92,7 +92,15 @@ enum CHAT_MESSAGE
EVIDENCE_ID,
FLIP,
REALIZATION,
TEXT_COLOR
TEXT_COLOR,
SHOWNAME,
OTHER_CHARID,
OTHER_NAME,
OTHER_EMOTE,
SELF_OFFSET,
OTHER_OFFSET,
OTHER_FLIP,
NONINTERRUPTING_PRE
};
enum COLOR
@ -103,7 +111,9 @@ enum COLOR
ORANGE,
BLUE,
YELLOW,
RAINBOW
RAINBOW,
PINK,
CYAN
};
#endif // DATATYPES_H

View File

@ -29,8 +29,8 @@ void Discord::state_lobby()
{
DiscordRichPresence presence;
std::memset(&presence, 0, sizeof(presence));
presence.largeImageKey = "ao2-logo";
presence.largeImageText = "Objection!";
presence.largeImageKey = "aa_cc_icon_new";
presence.largeImageText = "Omit!";
presence.instance = 1;
presence.state = "In Lobby";
@ -44,8 +44,8 @@ void Discord::state_server(std::string name, std::string server_id)
DiscordRichPresence presence;
std::memset(&presence, 0, sizeof(presence));
presence.largeImageKey = "ao2-logo";
presence.largeImageText = "Objection!";
presence.largeImageKey = "aa_cc_icon_new";
presence.largeImageText = "Omit!";
presence.instance = 1;
auto timestamp = static_cast<int64_t>(std::time(nullptr));
@ -70,8 +70,8 @@ void Discord::state_character(std::string name)
DiscordRichPresence presence;
std::memset(&presence, 0, sizeof(presence));
presence.largeImageKey = "ao2-logo";
presence.largeImageText = "Objection!";
presence.largeImageKey = "aa_cc_icon_new";
presence.largeImageText = "Omit!";
presence.instance = 1;
presence.details = this->server_name.c_str();
presence.matchSecret = this->server_id.c_str();
@ -89,8 +89,8 @@ void Discord::state_spectate()
DiscordRichPresence presence;
std::memset(&presence, 0, sizeof(presence));
presence.largeImageKey = "ao2-logo";
presence.largeImageText = "Objection!";
presence.largeImageKey = "aa_cc_icon_new";
presence.largeImageText = "Omit!";
presence.instance = 1;
presence.details = this->server_name.c_str();
presence.matchSecret = this->server_id.c_str();

View File

@ -7,12 +7,17 @@
#include <string>
#include <discord-rpc.h>
#include <cstring>
#include <ctime>
#include <QDebug>
namespace AttorneyOnline {
class Discord
{
private:
const char* APPLICATION_ID = "399779271737868288";
const char* APPLICATION_ID = "474362730397302823";
std::string server_name, server_id;
int64_t timestamp;
public:

View File

@ -2,8 +2,6 @@
#include "aoemotebutton.h"
#include <QDebug>
void Courtroom::construct_emotes()
{
ui_emotes = new QWidget(this);

View File

@ -2,12 +2,6 @@
#include "hex_functions.h"
#include <cstddef>
#include <stdlib.h>
#include <sstream>
#include <iomanip>
#include <QVector>
QString fanta_encrypt(QString temp_input, unsigned int p_key)
{
//using standard stdlib types is actually easier here because of implicit char<->int conversion

View File

@ -3,6 +3,12 @@
#include <QString>
#include <cstddef>
#include <stdlib.h>
#include <sstream>
#include <iomanip>
#include <QVector>
QString fanta_encrypt(QString p_input, unsigned int key);
QString fanta_decrypt(QString p_input, unsigned int key);

View File

@ -1,8 +1,5 @@
#include "courtroom.h"
#include <QDebug>
#include <QFileDialog>
void Courtroom::construct_evidence()
{
ui_evidence = new AOImage(this, ao_app);

View File

@ -1,6 +1,3 @@
#include <QFileInfo>
#include <QDir>
#include "file_functions.h"
bool file_exists(QString file_path)

View File

@ -1,6 +1,8 @@
#ifndef FILE_FUNCTIONS_H
#define FILE_FUNCTIONS_H
#include <QFileInfo>
#include <QDir>
#include <QString>
bool file_exists(QString file_path);

View File

@ -9,7 +9,7 @@ namespace omni
std::stringstream stream;
stream << std::setfill('0') << std::setw(sizeof(char)*2)
<< std::hex << input;
<< std::hex << input;
std::string result(stream.str());
std::transform(result.begin(), result.end(), result.begin(), ::toupper);

View File

@ -0,0 +1,26 @@
#pragma once
#if defined(DISCORD_DYNAMIC_LIB)
# if defined(_WIN32)
# if defined(DISCORD_BUILDING_SDK)
# define DISCORD_EXPORT __declspec(dllexport)
# else
# define DISCORD_EXPORT __declspec(dllimport)
# endif
# else
# define DISCORD_EXPORT __attribute__((visibility("default")))
# endif
#else
# define DISCORD_EXPORT
#endif
#ifdef __cplusplus
extern "C" {
#endif
DISCORD_EXPORT void Discord_Register(const char* applicationId, const char* command);
DISCORD_EXPORT void Discord_RegisterSteamGame(const char* applicationId, const char* steamId);
#ifdef __cplusplus
}
#endif

87
include/discord_rpc.h Normal file
View File

@ -0,0 +1,87 @@
#pragma once
#include <stdint.h>
// clang-format off
#if defined(DISCORD_DYNAMIC_LIB)
# if defined(_WIN32)
# if defined(DISCORD_BUILDING_SDK)
# define DISCORD_EXPORT __declspec(dllexport)
# else
# define DISCORD_EXPORT __declspec(dllimport)
# endif
# else
# define DISCORD_EXPORT __attribute__((visibility("default")))
# endif
#else
# define DISCORD_EXPORT
#endif
// clang-format on
#ifdef __cplusplus
extern "C" {
#endif
typedef struct DiscordRichPresence {
const char* state; /* max 128 bytes */
const char* details; /* max 128 bytes */
int64_t startTimestamp;
int64_t endTimestamp;
const char* largeImageKey; /* max 32 bytes */
const char* largeImageText; /* max 128 bytes */
const char* smallImageKey; /* max 32 bytes */
const char* smallImageText; /* max 128 bytes */
const char* partyId; /* max 128 bytes */
int partySize;
int partyMax;
const char* matchSecret; /* max 128 bytes */
const char* joinSecret; /* max 128 bytes */
const char* spectateSecret; /* max 128 bytes */
int8_t instance;
} DiscordRichPresence;
typedef struct DiscordUser {
const char* userId;
const char* username;
const char* discriminator;
const char* avatar;
} DiscordUser;
typedef struct DiscordEventHandlers {
void (*ready)(const DiscordUser* request);
void (*disconnected)(int errorCode, const char* message);
void (*errored)(int errorCode, const char* message);
void (*joinGame)(const char* joinSecret);
void (*spectateGame)(const char* spectateSecret);
void (*joinRequest)(const DiscordUser* request);
} DiscordEventHandlers;
#define DISCORD_REPLY_NO 0
#define DISCORD_REPLY_YES 1
#define DISCORD_REPLY_IGNORE 2
DISCORD_EXPORT void Discord_Initialize(const char* applicationId,
DiscordEventHandlers* handlers,
int autoRegister,
const char* optionalSteamId);
DISCORD_EXPORT void Discord_Shutdown(void);
/* checks for incoming messages, dispatches callbacks */
DISCORD_EXPORT void Discord_RunCallbacks(void);
/* If you disable the lib starting its own io thread, you'll need to call this from your own */
#ifdef DISCORD_DISABLE_IO_THREAD
DISCORD_EXPORT void Discord_UpdateConnection(void);
#endif
DISCORD_EXPORT void Discord_UpdatePresence(const DiscordRichPresence* presence);
DISCORD_EXPORT void Discord_ClearPresence(void);
DISCORD_EXPORT void Discord_Respond(const char* userid, /* DISCORD_REPLY_ */ int reply);
DISCORD_EXPORT void Discord_UpdateHandlers(DiscordEventHandlers* handlers);
#ifdef __cplusplus
} /* extern "C" */
#endif

View File

@ -5,14 +5,11 @@
#include "networkmanager.h"
#include "aosfxplayer.h"
#include <QDebug>
#include <QScrollBar>
Lobby::Lobby(AOApplication *p_ao_app) : QMainWindow()
{
ao_app = p_ao_app;
this->setWindowTitle("Attorney Online 2");
this->setWindowTitle("Attorney Online 2 -- Case Café Custom Client");
ui_background = new AOImage(this, ao_app);
ui_public_servers = new AOButton(this, ao_app);
@ -52,6 +49,8 @@ Lobby::Lobby(AOApplication *p_ao_app) : QMainWindow()
connect(ui_chatmessage, SIGNAL(returnPressed()), this, SLOT(on_chatfield_return_pressed()));
connect(ui_cancel, SIGNAL(clicked()), ao_app, SLOT(loading_cancelled()));
ui_connect->setEnabled(false);
set_widgets();
}
@ -99,7 +98,7 @@ void Lobby::set_widgets()
ui_connect->set_image("connect.png");
set_size_and_pos(ui_version, "version");
ui_version->setText("Version: " + ao_app->get_version_string());
ui_version->setText("AO Version: " + ao_app->get_version_string() + " | CCCC Version: " + ao_app->get_cccc_version_string());
set_size_and_pos(ui_about, "about");
ui_about->set_image("about.png");
@ -315,6 +314,8 @@ void Lobby::on_server_list_clicked(QModelIndex p_model)
ui_player_count->setText("Offline");
ui_connect->setEnabled(false);
ao_app->net_manager->connect_to_server(f_server);
}
@ -361,7 +362,7 @@ void Lobby::list_favorites()
void Lobby::append_chatmessage(QString f_name, QString f_message)
{
ui_chatbox->append_chatmessage(f_name, f_message);
ui_chatbox->append_chatmessage(f_name, f_message, ao_app->get_color("ooc_default_color", "courtroom_design.ini").name());
}
void Lobby::append_error(QString f_message)
@ -375,6 +376,11 @@ void Lobby::set_player_count(int players_online, int max_players)
ui_player_count->setText(f_string);
}
void Lobby::enable_connect_button()
{
ui_connect->setEnabled(true);
}
Lobby::~Lobby()
{

View File

@ -14,6 +14,9 @@
#include <QProgressBar>
#include <QTextBrowser>
#include <QDebug>
#include <QScrollBar>
class AOApplication;
class Lobby : public QMainWindow
@ -34,6 +37,7 @@ public:
void hide_loading_overlay(){ui_loading_background->hide();}
QString get_chatlog();
int get_selected_server();
void enable_connect_button();
void set_loading_value(int p_value);

View File

@ -1,8 +1,5 @@
#include "misc_functions.h"
#include <QTime>
#include <QCoreApplication>
void delay(int p_milliseconds)
{
QTime dieTime = QTime::currentTime().addMSecs(p_milliseconds);

View File

@ -1,6 +1,9 @@
#ifndef MISC_FUNCTIONS_H
#define MISC_FUNCTIONS_H
#include <QTime>
#include <QCoreApplication>
void delay(int p_milliseconds);
#endif // MISC_FUNCTIONS_H

View File

@ -4,8 +4,6 @@
#include "debug_functions.h"
#include "lobby.h"
#include <cstring>
NetworkManager::NetworkManager(AOApplication *parent) : QObject(parent)
{
ao_app = parent;
@ -21,10 +19,9 @@ NetworkManager::NetworkManager(AOApplication *parent) : QObject(parent)
QObject::connect(server_socket, SIGNAL(readyRead()), this, SLOT(handle_server_packet()));
QObject::connect(server_socket, SIGNAL(disconnected()), ao_app, SLOT(server_disconnected()));
QString master_config = ao_app->read_config("master");
if (master_config != "") {
QString master_config = ao_app->configini->value("master", "").value<QString>();
if (master_config != "")
ms_nosrv_hostname = master_config;
}
}
NetworkManager::~NetworkManager()

View File

@ -21,6 +21,7 @@
#include <QDnsLookup>
#include <QTime>
#include <QTimer>
#include <cstring>
class NetworkManager : public QObject
{

View File

@ -7,9 +7,6 @@
#include "hardware_functions.h"
#include "debug_functions.h"
#include <QDebug>
#include <QCryptographicHash>
void AOApplication::ms_packet_received(AOPacket *p_packet)
{
p_packet->net_decode();
@ -150,6 +147,10 @@ void AOApplication::server_packet_received(AOPacket *p_packet)
improved_loading_enabled = false;
desk_mod_enabled = false;
evidence_enabled = false;
cccc_ic_support_enabled = false;
arup_enabled = false;
casing_alerts_enabled = false;
modcall_reason_enabled = false;
//workaround for tsuserver4
if (f_contents.at(0) == "NOENCRYPT")
@ -177,7 +178,12 @@ void AOApplication::server_packet_received(AOPacket *p_packet)
goto end;
if (courtroom_constructed)
w_courtroom->append_server_chatmessage(f_contents.at(0), f_contents.at(1));
{
if (f_contents.size() == 3)
w_courtroom->append_server_chatmessage(f_contents.at(0), f_contents.at(1), f_contents.at(2));
else
w_courtroom->append_server_chatmessage(f_contents.at(0), f_contents.at(1), "0");
}
}
else if (header == "FL")
{
@ -195,6 +201,16 @@ void AOApplication::server_packet_received(AOPacket *p_packet)
desk_mod_enabled = true;
if (f_packet.contains("evidence",Qt::CaseInsensitive))
evidence_enabled = true;
if (f_packet.contains("cccc_ic_support",Qt::CaseInsensitive))
cccc_ic_support_enabled = true;
if (f_packet.contains("arup",Qt::CaseInsensitive))
arup_enabled = true;
if (f_packet.contains("casing_alerts",Qt::CaseInsensitive))
casing_alerts_enabled = true;
if (f_packet.contains("modcall_reason",Qt::CaseInsensitive))
modcall_reason_enabled = true;
w_lobby->enable_connect_button();
}
else if (header == "PN")
{
@ -218,13 +234,14 @@ void AOApplication::server_packet_received(AOPacket *p_packet)
loaded_chars = 0;
loaded_evidence = 0;
loaded_music = 0;
generated_chars = 0;
destruct_courtroom();
construct_courtroom();
courtroom_loaded = false;
QString window_title = "Attorney Online 2";
QString window_title = "Attorney Online 2 -- Case Café Custom Client";
int selected_server = w_lobby->get_selected_server();
QString server_address = "", server_name = "";
@ -264,7 +281,7 @@ void AOApplication::server_packet_received(AOPacket *p_packet)
QCryptographicHash hash(QCryptographicHash::Algorithm::Sha256);
hash.addData(server_address.toUtf8());
if(is_discord_enabled())
if (is_discord_enabled())
discord->state_server(server_name.toStdString(), hash.result().toBase64().toStdString());
}
else if (header == "CI")
@ -300,7 +317,8 @@ void AOApplication::server_packet_received(AOPacket *p_packet)
w_courtroom->append_char(f_char);
int loading_value = (loaded_chars / static_cast<double>(total_loading_size)) * 100;
int total_loading_size = char_list_size * 2 + evidence_list_size + music_list_size;
int loading_value = int(((loaded_chars + generated_chars + loaded_music + loaded_evidence) / static_cast<double>(total_loading_size)) * 100);
w_lobby->set_loading_value(loading_value);
}
@ -343,8 +361,8 @@ void AOApplication::server_packet_received(AOPacket *p_packet)
w_courtroom->append_evidence(f_evi);
int total_loading_size = char_list_size + evidence_list_size + music_list_size;
int loading_value = ((loaded_chars + loaded_evidence) / static_cast<double>(total_loading_size)) * 100;
int total_loading_size = char_list_size * 2 + evidence_list_size + music_list_size;
int loading_value = int(((loaded_chars + generated_chars + loaded_music + loaded_evidence) / static_cast<double>(total_loading_size)) * 100);
w_lobby->set_loading_value(loading_value);
QString next_packet_number = QString::number(loaded_evidence);
@ -356,7 +374,8 @@ void AOApplication::server_packet_received(AOPacket *p_packet)
if (!courtroom_constructed)
goto end;
int total_loading_size = char_list_size + evidence_list_size + music_list_size;
bool musics_time = false;
int areas = 0;
for (int n_element = 0 ; n_element < f_contents.size() ; n_element += 2)
{
@ -372,12 +391,40 @@ void AOApplication::server_packet_received(AOPacket *p_packet)
w_lobby->set_loading_text("Loading music:\n" + QString::number(loaded_music) + "/" + QString::number(music_list_size));
w_courtroom->append_music(f_music);
if (musics_time)
{
w_courtroom->append_music(f_music);
}
else
{
if (f_music.endsWith(".wav") ||
f_music.endsWith(".mp3") ||
f_music.endsWith(".mp4") ||
f_music.endsWith(".ogg") ||
f_music.endsWith(".opus"))
{
musics_time = true;
areas--;
w_courtroom->fix_last_area();
w_courtroom->append_music(f_music);
}
else
{
w_courtroom->append_area(f_music);
areas++;
}
}
int loading_value = ((loaded_chars + loaded_evidence + loaded_music) / static_cast<double>(total_loading_size)) * 100;
for (int area_n = 0; area_n < areas; area_n++)
{
w_courtroom->arup_append(0, "Unknown", "Unknown", "Unknown");
}
int total_loading_size = char_list_size * 2 + evidence_list_size + music_list_size;
int loading_value = int(((loaded_chars + generated_chars + loaded_music + loaded_evidence) / static_cast<double>(total_loading_size)) * 100);
w_lobby->set_loading_value(loading_value);
}
QString next_packet_number = QString::number(((loaded_music - 1) / 10) + 1);
send_server_packet(new AOPacket("AM#" + next_packet_number + "#%"));
}
@ -420,7 +467,8 @@ void AOApplication::server_packet_received(AOPacket *p_packet)
w_courtroom->append_char(f_char);
int loading_value = ((loaded_chars + loaded_evidence + loaded_music) / static_cast<double>(total_loading_size)) * 100;
int total_loading_size = char_list_size * 2 + evidence_list_size + music_list_size;
int loading_value = int(((loaded_chars + generated_chars + loaded_music + loaded_evidence) / static_cast<double>(total_loading_size)) * 100);
w_lobby->set_loading_value(loading_value);
}
@ -432,6 +480,8 @@ void AOApplication::server_packet_received(AOPacket *p_packet)
goto end;
int total_loading_size = char_list_size + evidence_list_size + music_list_size;
bool musics_time = false;
int areas = 0;
for (int n_element = 0 ; n_element < f_contents.size() ; ++n_element)
{
@ -441,7 +491,37 @@ void AOApplication::server_packet_received(AOPacket *p_packet)
w_courtroom->append_music(f_contents.at(n_element));
int loading_value = ((loaded_chars + loaded_evidence + loaded_music) / static_cast<double>(total_loading_size)) * 100;
if (musics_time)
{
w_courtroom->append_music(f_contents.at(n_element));
}
else
{
if (f_contents.at(n_element).endsWith(".wav") ||
f_contents.at(n_element).endsWith(".mp3") ||
f_contents.at(n_element).endsWith(".mp4") ||
f_contents.at(n_element).endsWith(".ogg") ||
f_contents.at(n_element).endsWith(".opus"))
{
musics_time = true;
w_courtroom->fix_last_area();
w_courtroom->append_music(f_contents.at(n_element));
areas--;
}
else
{
w_courtroom->append_area(f_contents.at(n_element));
areas++;
}
}
for (int area_n = 0; area_n < areas; area_n++)
{
w_courtroom->arup_append(0, "Unknown", "Unknown", "Unknown");
}
int total_loading_size = char_list_size * 2 + evidence_list_size + music_list_size;
int loading_value = int(((loaded_chars + generated_chars + loaded_music + loaded_evidence) / static_cast<double>(total_loading_size)) * 100);
w_lobby->set_loading_value(loading_value);
}
@ -455,6 +535,7 @@ void AOApplication::server_packet_received(AOPacket *p_packet)
if (lobby_constructed)
w_courtroom->append_ms_chatmessage("", w_lobby->get_chatlog());
w_courtroom->character_loading_finished();
w_courtroom->done_received();
courtroom_loaded = true;
@ -493,7 +574,13 @@ void AOApplication::server_packet_received(AOPacket *p_packet)
if (f_contents.size() < 1)
goto end;
if (courtroom_constructed)
w_courtroom->handle_wtce(f_contents.at(0));
{
if (f_contents.size() == 1)
w_courtroom->handle_wtce(f_contents.at(0), 0);
else if (f_contents.size() == 2) {
w_courtroom->handle_wtce(f_contents.at(0), f_contents.at(1).toInt());
}
}
}
else if (header == "HP")
{
@ -524,6 +611,17 @@ void AOApplication::server_packet_received(AOPacket *p_packet)
w_courtroom->set_evidence_list(f_evi_list);
}
}
else if (header == "ARUP")
{
if (courtroom_constructed)
{
int arup_type = f_contents.at(0).toInt();
for (int n_element = 1 ; n_element < f_contents.size() ; n_element++)
{
w_courtroom->arup_modify(arup_type, n_element - 1, f_contents.at(n_element));
}
}
}
else if (header == "IL")
{
if (courtroom_constructed && f_contents.size() > 0)
@ -569,6 +667,11 @@ void AOApplication::server_packet_received(AOPacket *p_packet)
if (courtroom_constructed && f_contents.size() > 0)
w_courtroom->mod_called(f_contents.at(0));
}
else if (header == "CASEA")
{
if (courtroom_constructed && f_contents.size() > 6)
w_courtroom->case_called(f_contents.at(0), f_contents.at(1) == "1", f_contents.at(2) == "1", f_contents.at(3) == "1", f_contents.at(4) == "1", f_contents.at(5) == "1");
}
end:

View File

@ -1,9 +1,6 @@
#include "aoapplication.h"
#include "courtroom.h"
#include "file_functions.h"
#include <QDir>
#include <QDebug>
#include <QStandardPaths>
#ifdef BASE_OVERRIDE
#include "base_override.h"

0
server/__init__.py Normal file
View File

807
server/aoprotocol.py Normal file
View File

@ -0,0 +1,807 @@
# tsuserver3, an Attorney Online server
#
# Copyright (C) 2016 argoneus <argoneuscze@gmail.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import asyncio
import re
from time import localtime, strftime
from enum import Enum
from . import commands
from . import logger
from .exceptions import ClientError, AreaError, ArgumentError, ServerError
from .fantacrypt import fanta_decrypt
from .evidence import EvidenceList
from .websocket import WebSocket
import unicodedata
class AOProtocol(asyncio.Protocol):
"""
The main class that deals with the AO protocol.
"""
class ArgType(Enum):
STR = 1,
STR_OR_EMPTY = 2,
INT = 3
def __init__(self, server):
super().__init__()
self.server = server
self.client = None
self.buffer = ''
self.ping_timeout = None
self.websocket = None
def data_received(self, data):
""" Handles any data received from the network.
Receives data, parses them into a command and passes it
to the command handler.
:param data: bytes of data
"""
if self.websocket is None:
self.websocket = WebSocket(self.client, self)
if not self.websocket.handshake(data):
self.websocket = False
else:
self.client.websocket = self.websocket
buf = data
if not self.client.is_checked and self.server.ban_manager.is_banned(self.client.ipid):
self.client.transport.close()
else:
self.client.is_checked = True
if self.websocket:
buf = self.websocket.handle(data)
if buf is None:
buf = b''
if not isinstance(buf, str):
# try to decode as utf-8, ignore any erroneous characters
self.buffer += buf.decode('utf-8', 'ignore')
else:
self.buffer = buf
if len(self.buffer) > 8192:
self.client.disconnect()
for msg in self.get_messages():
if len(msg) < 2:
continue
# general netcode structure is not great
if msg[0] in ('#', '3', '4'):
if msg[0] == '#':
msg = msg[1:]
spl = msg.split('#', 1)
msg = '#'.join([fanta_decrypt(spl[0])] + spl[1:])
logger.log_debug('[INC][RAW]{}'.format(msg), self.client)
try:
cmd, *args = msg.split('#')
self.net_cmd_dispatcher[cmd](self, args)
except KeyError:
logger.log_debug('[INC][UNK]{}'.format(msg), self.client)
def connection_made(self, transport):
""" Called upon a new client connecting
:param transport: the transport object
"""
self.client = self.server.new_client(transport)
self.ping_timeout = asyncio.get_event_loop().call_later(self.server.config['timeout'], self.client.disconnect)
asyncio.get_event_loop().call_later(0.25, self.client.send_command, 'decryptor', 34) # just fantacrypt things)
def connection_lost(self, exc):
""" User disconnected
:param exc: reason
"""
self.server.remove_client(self.client)
self.ping_timeout.cancel()
def get_messages(self):
""" Parses out full messages from the buffer.
:return: yields messages
"""
while '#%' in self.buffer:
spl = self.buffer.split('#%', 1)
self.buffer = spl[1]
yield spl[0]
# exception because bad netcode
askchar2 = '#615810BC07D12A5A#'
if self.buffer == askchar2:
self.buffer = ''
yield askchar2
def validate_net_cmd(self, args, *types, needs_auth=True):
""" Makes sure the net command's arguments match expectations.
:param args: actual arguments to the net command
:param types: what kind of data types are expected
:param needs_auth: whether you need to have chosen a character
:return: returns True if message was validated
"""
if needs_auth and self.client.char_id == -1:
return False
if len(args) != len(types):
return False
for i, arg in enumerate(args):
if len(arg) == 0 and types[i] != self.ArgType.STR_OR_EMPTY:
return False
if types[i] == self.ArgType.INT:
try:
args[i] = int(arg)
except ValueError:
return False
return True
def net_cmd_hi(self, args):
""" Handshake.
HI#<hdid:string>#%
:param args: a list containing all the arguments
"""
if not self.validate_net_cmd(args, self.ArgType.STR, needs_auth=False):
return
self.client.hdid = args[0]
if self.client.hdid not in self.client.server.hdid_list:
self.client.server.hdid_list[self.client.hdid] = []
if self.client.ipid not in self.client.server.hdid_list[self.client.hdid]:
self.client.server.hdid_list[self.client.hdid].append(self.client.ipid)
self.client.server.dump_hdids()
for ipid in self.client.server.hdid_list[self.client.hdid]:
if self.server.ban_manager.is_banned(ipid):
self.client.send_command('BD')
self.client.disconnect()
return
logger.log_server('Connected. HDID: {}.'.format(self.client.hdid), self.client)
self.client.send_command('ID', self.client.id, self.server.software, self.server.get_version_string())
self.client.send_command('PN', self.server.get_player_count() - 1, self.server.config['playerlimit'])
def net_cmd_id(self, args):
""" Client version and PV
ID#<pv:int>#<software:string>#<version:string>#%
"""
self.client.is_ao2 = False
if len(args) < 2:
return
version_list = args[1].split('.')
if len(version_list) < 3:
return
release = int(version_list[0])
major = int(version_list[1])
minor = int(version_list[2])
if args[0] != 'AO2':
return
if release < 2:
return
elif release == 2:
if major < 2:
return
elif major == 2:
if minor < 5:
return
self.client.is_ao2 = True
self.client.send_command('FL', 'yellowtext', 'customobjections', 'flipping', 'fastloading', 'noencryption', 'deskmod', 'evidence', 'modcall_reason', 'cccc_ic_support', 'arup', 'casing_alerts')
def net_cmd_ch(self, _):
""" Periodically checks the connection.
CHECK#%
"""
self.client.send_command('CHECK')
self.ping_timeout.cancel()
self.ping_timeout = asyncio.get_event_loop().call_later(self.server.config['timeout'], self.client.disconnect)
def net_cmd_askchaa(self, _):
""" Ask for the counts of characters/evidence/music
askchaa#%
"""
char_cnt = len(self.server.char_list)
evi_cnt = 0
music_cnt = sum([len(x) for x in self.server.music_pages_ao1])
self.client.send_command('SI', char_cnt, evi_cnt, music_cnt)
def net_cmd_askchar2(self, _):
""" Asks for the character list.
askchar2#%
"""
self.client.send_command('CI', *self.server.char_pages_ao1[0])
def net_cmd_an(self, args):
""" Asks for specific pages of the character list.
AN#<page:int>#%
"""
if not self.validate_net_cmd(args, self.ArgType.INT, needs_auth=False):
return
if len(self.server.char_pages_ao1) > args[0] >= 0:
self.client.send_command('CI', *self.server.char_pages_ao1[args[0]])
else:
self.client.send_command('EM', *self.server.music_pages_ao1[0])
def net_cmd_ae(self, _):
""" Asks for specific pages of the evidence list.
AE#<page:int>#%
"""
pass # todo evidence maybe later
def net_cmd_am(self, args):
""" Asks for specific pages of the music list.
AM#<page:int>#%
"""
if not self.validate_net_cmd(args, self.ArgType.INT, needs_auth=False):
return
if len(self.server.music_pages_ao1) > args[0] >= 0:
self.client.send_command('EM', *self.server.music_pages_ao1[args[0]])
else:
self.client.send_done()
self.client.send_area_list()
self.client.send_motd()
def net_cmd_rc(self, _):
""" Asks for the whole character list(AO2)
AC#%
"""
self.client.send_command('SC', *self.server.char_list)
def net_cmd_rm(self, _):
""" Asks for the whole music list(AO2)
AM#%
"""
self.client.send_command('SM', *self.server.music_list_ao2)
def net_cmd_rd(self, _):
""" Asks for server metadata(charscheck, motd etc.) and a DONE#% signal(also best packet)
RD#%
"""
self.client.send_done()
self.client.send_area_list()
self.client.send_motd()
def net_cmd_cc(self, args):
""" Character selection.
CC#<client_id:int>#<char_id:int>#<hdid:string>#%
"""
if not self.validate_net_cmd(args, self.ArgType.INT, self.ArgType.INT, self.ArgType.STR, needs_auth=False):
return
cid = args[1]
try:
self.client.change_character(cid)
except ClientError:
return
def net_cmd_ms(self, args):
""" IC message.
Refer to the implementation for details.
"""
if self.client.is_muted: # Checks to see if the client has been muted by a mod
self.client.send_host_message("You have been muted by a moderator")
return
if not self.client.area.can_send_message(self.client):
return
target_area = []
if self.validate_net_cmd(args, self.ArgType.STR, self.ArgType.STR_OR_EMPTY, self.ArgType.STR,
self.ArgType.STR,
self.ArgType.STR, self.ArgType.STR, self.ArgType.STR, self.ArgType.INT,
self.ArgType.INT, self.ArgType.INT, self.ArgType.INT, self.ArgType.INT,
self.ArgType.INT, self.ArgType.INT, self.ArgType.INT):
# Vanilla validation monstrosity.
msg_type, pre, folder, anim, text, pos, sfx, anim_type, cid, sfx_delay, button, evidence, flip, ding, color = args
showname = ""
charid_pair = -1
offset_pair = 0
nonint_pre = 0
elif self.validate_net_cmd(args, self.ArgType.STR, self.ArgType.STR_OR_EMPTY, self.ArgType.STR,
self.ArgType.STR,
self.ArgType.STR, self.ArgType.STR, self.ArgType.STR, self.ArgType.INT,
self.ArgType.INT, self.ArgType.INT, self.ArgType.INT, self.ArgType.INT,
self.ArgType.INT, self.ArgType.INT, self.ArgType.INT, self.ArgType.STR_OR_EMPTY):
# 1.3.0 validation monstrosity.
msg_type, pre, folder, anim, text, pos, sfx, anim_type, cid, sfx_delay, button, evidence, flip, ding, color, showname = args
charid_pair = -1
offset_pair = 0
nonint_pre = 0
if len(showname) > 0 and not self.client.area.showname_changes_allowed:
self.client.send_host_message("Showname changes are forbidden in this area!")
return
elif self.validate_net_cmd(args, self.ArgType.STR, self.ArgType.STR_OR_EMPTY, self.ArgType.STR,
self.ArgType.STR,
self.ArgType.STR, self.ArgType.STR, self.ArgType.STR, self.ArgType.INT,
self.ArgType.INT, self.ArgType.INT, self.ArgType.INT, self.ArgType.INT,
self.ArgType.INT, self.ArgType.INT, self.ArgType.INT, self.ArgType.STR_OR_EMPTY, self.ArgType.INT, self.ArgType.INT):
# 1.3.5 validation monstrosity.
msg_type, pre, folder, anim, text, pos, sfx, anim_type, cid, sfx_delay, button, evidence, flip, ding, color, showname, charid_pair, offset_pair = args
nonint_pre = 0
if len(showname) > 0 and not self.client.area.showname_changes_allowed:
self.client.send_host_message("Showname changes are forbidden in this area!")
return
elif self.validate_net_cmd(args, self.ArgType.STR, self.ArgType.STR_OR_EMPTY, self.ArgType.STR,
self.ArgType.STR,
self.ArgType.STR, self.ArgType.STR, self.ArgType.STR, self.ArgType.INT,
self.ArgType.INT, self.ArgType.INT, self.ArgType.INT, self.ArgType.INT,
self.ArgType.INT, self.ArgType.INT, self.ArgType.INT, self.ArgType.STR_OR_EMPTY, self.ArgType.INT, self.ArgType.INT, self.ArgType.INT):
# 1.4.0 validation monstrosity.
msg_type, pre, folder, anim, text, pos, sfx, anim_type, cid, sfx_delay, button, evidence, flip, ding, color, showname, charid_pair, offset_pair, nonint_pre = args
if len(showname) > 0 and not self.client.area.showname_changes_allowed:
self.client.send_host_message("Showname changes are forbidden in this area!")
return
else:
return
if self.client.area.is_iniswap(self.client, pre, anim, folder) and folder != self.client.get_char_name():
self.client.send_host_message("Iniswap is blocked in this area")
return
if len(self.client.charcurse) > 0 and folder != self.client.get_char_name():
self.client.send_host_message("You may not iniswap while you are charcursed!")
return
if not self.client.area.blankposting_allowed:
if text == ' ':
self.client.send_host_message("Blankposting is forbidden in this area!")
return
if text.isspace():
self.client.send_host_message("Blankposting is forbidden in this area, and putting more spaces in does not make it not blankposting.")
return
if len(re.sub(r'[{}\\`|(~~)]','', text).replace(' ', '')) < 3 and text != '<' and text != '>':
self.client.send_host_message("While that is not a blankpost, it is still pretty spammy. Try forming sentences.")
return
if text.startswith('/a '):
part = text.split(' ')
try:
aid = int(part[1])
if self.client in self.server.area_manager.get_area_by_id(aid).owners:
target_area.append(aid)
if not target_area:
self.client.send_host_message('You don\'t own {}!'.format(self.server.area_manager.get_area_by_id(aid).name))
return
text = ' '.join(part[2:])
except ValueError:
self.client.send_host_message("That does not look like a valid area ID!")
return
elif text.startswith('/s '):
part = text.split(' ')
for a in self.server.area_manager.areas:
if self.client in a.owners:
target_area.append(a.id)
if not target_area:
self.client.send_host_message('You don\'t any areas!')
return
text = ' '.join(part[1:])
if msg_type not in ('chat', '0', '1'):
return
if anim_type not in (0, 1, 2, 5, 6):
return
if cid != self.client.char_id:
return
if sfx_delay < 0:
return
if button not in (0, 1, 2, 3, 4):
return
if evidence < 0:
return
if ding not in (0, 1):
return
if color not in (0, 1, 2, 3, 4, 5, 6, 7, 8):
return
if len(showname) > 15:
self.client.send_host_message("Your IC showname is way too long!")
return
if nonint_pre == 1:
if button in (1, 2, 3, 4, 23):
if anim_type == 1 or anim_type == 2:
anim_type = 0
elif anim_type == 6:
anim_type = 5
if self.client.area.non_int_pres_only:
if anim_type == 1 or anim_type == 2:
anim_type = 0
nonint_pre = 1
elif anim_type == 6:
anim_type = 5
nonint_pre = 1
if not self.client.area.shouts_allowed:
# Old clients communicate the objecting in anim_type.
if anim_type == 2:
anim_type = 1
elif anim_type == 6:
anim_type = 5
# New clients do it in a specific objection message area.
button = 0
# Turn off the ding.
ding = 0
if color == 2 and not (self.client.is_mod or self.client in self.client.area.owners):
color = 0
if color == 6:
text = re.sub(r'[^\x00-\x7F]+',' ', text) #remove all unicode to prevent redtext abuse
if len(text.strip( ' ' )) == 1:
color = 0
else:
if text.strip( ' ' ) in ('<num>', '<percent>', '<dollar>', '<and>'):
color = 0
if self.client.pos:
pos = self.client.pos
else:
if pos not in ('def', 'pro', 'hld', 'hlp', 'jud', 'wit', 'jur', 'sea'):
return
msg = text[:256]
if self.client.shaken:
msg = self.client.shake_message(msg)
if self.client.disemvowel:
msg = self.client.disemvowel_message(msg)
self.client.pos = pos
if evidence:
if self.client.area.evi_list.evidences[self.client.evi_list[evidence] - 1].pos != 'all':
self.client.area.evi_list.evidences[self.client.evi_list[evidence] - 1].pos = 'all'
self.client.area.broadcast_evidence_list()
# Here, we check the pair stuff, and save info about it to the client.
# Notably, while we only get a charid_pair and an offset, we send back a chair_pair, an emote, a talker offset
# and an other offset.
self.client.charid_pair = charid_pair
self.client.offset_pair = offset_pair
if anim_type not in (5, 6):
self.client.last_sprite = anim
self.client.flip = flip
self.client.claimed_folder = folder
other_offset = 0
other_emote = ''
other_flip = 0
other_folder = ''
confirmed = False
if charid_pair > -1:
for target in self.client.area.clients:
if target.char_id == self.client.charid_pair and target.charid_pair == self.client.char_id and target != self.client and target.pos == self.client.pos:
confirmed = True
other_offset = target.offset_pair
other_emote = target.last_sprite
other_flip = target.flip
other_folder = target.claimed_folder
break
if not confirmed:
charid_pair = -1
offset_pair = 0
self.client.area.send_command('MS', msg_type, pre, folder, anim, msg, pos, sfx, anim_type, cid,
sfx_delay, button, self.client.evi_list[evidence], flip, ding, color, showname,
charid_pair, other_folder, other_emote, offset_pair, other_offset, other_flip, nonint_pre)
self.client.area.send_owner_command('MS', msg_type, pre, folder, anim, '[' + self.client.area.abbreviation + ']' + msg, pos, sfx, anim_type, cid,
sfx_delay, button, self.client.evi_list[evidence], flip, ding, color, showname,
charid_pair, other_folder, other_emote, offset_pair, other_offset, other_flip, nonint_pre)
self.server.area_manager.send_remote_command(target_area, 'MS', msg_type, pre, folder, anim, msg, pos, sfx, anim_type, cid,
sfx_delay, button, self.client.evi_list[evidence], flip, ding, color, showname,
charid_pair, other_folder, other_emote, offset_pair, other_offset, other_flip, nonint_pre)
self.client.area.set_next_msg_delay(len(msg))
logger.log_server('[IC][{}][{}]{}'.format(self.client.area.abbreviation, self.client.get_char_name(), msg), self.client)
if (self.client.area.is_recording):
self.client.area.recorded_messages.append(args)
def net_cmd_ct(self, args):
""" OOC Message
CT#<name:string>#<message:string>#%
"""
if self.client.is_ooc_muted: # Checks to see if the client has been muted by a mod
self.client.send_host_message("You have been muted by a moderator")
return
if not self.validate_net_cmd(args, self.ArgType.STR, self.ArgType.STR):
return
if self.client.name != args[0] and self.client.fake_name != args[0]:
if self.client.is_valid_name(args[0]):
self.client.name = args[0]
self.client.fake_name = args[0]
else:
self.client.fake_name = args[0]
if self.client.name == '':
self.client.send_host_message('You must insert a name with at least one letter')
return
if len(self.client.name) > 30:
self.client.send_host_message('Your OOC name is too long! Limit it to 30 characters.')
return
for c in self.client.name:
if unicodedata.category(c) == 'Cf':
self.client.send_host_message('You cannot use format characters in your name!')
return
if self.client.name.startswith(self.server.config['hostname']) or self.client.name.startswith('<dollar>G') or self.client.name.startswith('<dollar>M'):
self.client.send_host_message('That name is reserved!')
return
if args[1].startswith(' /'):
self.client.send_host_message('Your message was not sent for safety reasons: you left a space before that slash.')
return
if args[1].startswith('/'):
spl = args[1][1:].split(' ', 1)
cmd = spl[0].lower()
arg = ''
if len(spl) == 2:
arg = spl[1][:256]
try:
called_function = 'ooc_cmd_{}'.format(cmd)
getattr(commands, called_function)(self.client, arg)
except AttributeError:
print('Attribute error with ' + called_function)
self.client.send_host_message('Invalid command.')
except (ClientError, AreaError, ArgumentError, ServerError) as ex:
self.client.send_host_message(ex)
else:
if self.client.shaken:
args[1] = self.client.shake_message(args[1])
if self.client.disemvowel:
args[1] = self.client.disemvowel_message(args[1])
self.client.area.send_command('CT', self.client.name, args[1])
self.client.area.send_owner_command('CT', '[' + self.client.area.abbreviation + ']' + self.client.name, args[1])
logger.log_server(
'[OOC][{}][{}]{}'.format(self.client.area.abbreviation, self.client.get_char_name(),
args[1]), self.client)
def net_cmd_mc(self, args):
""" Play music.
MC#<song_name:int>#<???:int>#%
"""
try:
area = self.server.area_manager.get_area_by_name(args[0])
self.client.change_area(area)
except AreaError:
if self.client.is_muted: # Checks to see if the client has been muted by a mod
self.client.send_host_message("You have been muted by a moderator")
return
if not self.client.is_dj:
self.client.send_host_message('You were blockdj\'d by a moderator.')
return
if self.client.area.cannot_ic_interact(self.client):
self.client.send_host_message("You are not on the area's invite list, and thus, you cannot change music!")
return
if not self.validate_net_cmd(args, self.ArgType.STR, self.ArgType.INT) and not self.validate_net_cmd(args, self.ArgType.STR, self.ArgType.INT, self.ArgType.STR):
return
if args[1] != self.client.char_id:
return
if self.client.change_music_cd():
self.client.send_host_message('You changed song too many times. Please try again after {} seconds.'.format(int(self.client.change_music_cd())))
return
try:
name, length = self.server.get_song_data(args[0])
if self.client.area.jukebox:
showname = ''
if len(args) > 2:
showname = args[2]
if len(showname) > 0 and not self.client.area.showname_changes_allowed:
self.client.send_host_message("Showname changes are forbidden in this area!")
return
self.client.area.add_jukebox_vote(self.client, name, length, showname)
logger.log_server('[{}][{}]Added a jukebox vote for {}.'.format(self.client.area.abbreviation, self.client.get_char_name(), name), self.client)
else:
if len(args) > 2:
showname = args[2]
if len(showname) > 0 and not self.client.area.showname_changes_allowed:
self.client.send_host_message("Showname changes are forbidden in this area!")
return
self.client.area.play_music_shownamed(name, self.client.char_id, showname, length)
self.client.area.add_music_playing_shownamed(self.client, showname, name)
else:
self.client.area.play_music(name, self.client.char_id, length)
self.client.area.add_music_playing(self.client, name)
logger.log_server('[{}][{}]Changed music to {}.'
.format(self.client.area.abbreviation, self.client.get_char_name(), name), self.client)
except ServerError:
return
except ClientError as ex:
self.client.send_host_message(ex)
def net_cmd_rt(self, args):
""" Plays the Testimony/CE animation.
RT#<type:string>#%
"""
if not self.client.area.shouts_allowed:
self.client.send_host_message("You cannot use the testimony buttons here!")
return
if self.client.is_muted: # Checks to see if the client has been muted by a mod
self.client.send_host_message("You have been muted by a moderator")
return
if not self.client.can_wtce:
self.client.send_host_message('You were blocked from using judge signs by a moderator.')
return
if self.client.area.cannot_ic_interact(self.client):
self.client.send_host_message("You are not on the area's invite list, and thus, you cannot use the WTCE buttons!")
return
if not self.validate_net_cmd(args, self.ArgType.STR) and not self.validate_net_cmd(args, self.ArgType.STR, self.ArgType.INT):
return
if args[0] == 'testimony1':
sign = 'WT'
elif args[0] == 'testimony2':
sign = 'CE'
elif args[0] == 'judgeruling':
sign = 'JR'
else:
return
if self.client.wtce_mute():
self.client.send_host_message('You used witness testimony/cross examination signs too many times. Please try again after {} seconds.'.format(int(self.client.wtce_mute())))
return
if len(args) == 1:
self.client.area.send_command('RT', args[0])
elif len(args) == 2:
self.client.area.send_command('RT', args[0], args[1])
self.client.area.add_to_judgelog(self.client, 'used {}'.format(sign))
logger.log_server("[{}]{} Used WT/CE".format(self.client.area.abbreviation, self.client.get_char_name()), self.client)
def net_cmd_hp(self, args):
""" Sets the penalty bar.
HP#<type:int>#<new_value:int>#%
"""
if self.client.is_muted: # Checks to see if the client has been muted by a mod
self.client.send_host_message("You have been muted by a moderator")
return
if self.client.area.cannot_ic_interact(self.client):
self.client.send_host_message("You are not on the area's invite list, and thus, you cannot change the Confidence bars!")
return
if not self.validate_net_cmd(args, self.ArgType.INT, self.ArgType.INT):
return
try:
self.client.area.change_hp(args[0], args[1])
self.client.area.add_to_judgelog(self.client, 'changed the penalties')
logger.log_server('[{}]{} changed HP ({}) to {}'
.format(self.client.area.abbreviation, self.client.get_char_name(), args[0], args[1]), self.client)
except AreaError:
return
def net_cmd_pe(self, args):
""" Adds a piece of evidence.
PE#<name: string>#<description: string>#<image: string>#%
"""
if len(args) < 3:
return
#evi = Evidence(args[0], args[1], args[2], self.client.pos)
self.client.area.evi_list.add_evidence(self.client, args[0], args[1], args[2], 'all')
self.client.area.broadcast_evidence_list()
def net_cmd_de(self, args):
""" Deletes a piece of evidence.
DE#<id: int>#%
"""
self.client.area.evi_list.del_evidence(self.client, self.client.evi_list[int(args[0])])
self.client.area.broadcast_evidence_list()
def net_cmd_ee(self, args):
""" Edits a piece of evidence.
EE#<id: int>#<name: string>#<description: string>#<image: string>#%
"""
if len(args) < 4:
return
evi = (args[1], args[2], args[3], 'all')
self.client.area.evi_list.edit_evidence(self.client, self.client.evi_list[int(args[0])], evi)
self.client.area.broadcast_evidence_list()
def net_cmd_zz(self, args):
""" Sent on mod call.
"""
if self.client.is_muted: # Checks to see if the client has been muted by a mod
self.client.send_host_message("You have been muted by a moderator")
return
if not self.client.can_call_mod():
self.client.send_host_message("You must wait 30 seconds between mod calls.")
return
current_time = strftime("%H:%M", localtime())
if len(args) < 1:
self.server.send_all_cmd_pred('ZZ', '[{}] {} ({}) in {} without reason (not using the Case Café client?)'
.format(current_time, self.client.get_char_name(), self.client.get_ip(), self.client.area.name), pred=lambda c: c.is_mod)
self.client.set_mod_call_delay()
logger.log_server('[{}]{} called a moderator.'.format(self.client.area.abbreviation, self.client.get_char_name()), self.client)
else:
self.server.send_all_cmd_pred('ZZ', '[{}] {} ({}) in {} with reason: {}'
.format(current_time, self.client.get_char_name(), self.client.get_ip(), self.client.area.name, args[0][:100]), pred=lambda c: c.is_mod)
self.client.set_mod_call_delay()
logger.log_server('[{}]{} called a moderator: {}.'.format(self.client.area.abbreviation, self.client.get_char_name(), args[0]), self.client)
def net_cmd_opKICK(self, args):
self.net_cmd_ct(['opkick', '/kick {}'.format(args[0])])
def net_cmd_opBAN(self, args):
self.net_cmd_ct(['opban', '/ban {}'.format(args[0])])
net_cmd_dispatcher = {
'HI': net_cmd_hi, # handshake
'ID': net_cmd_id, # client version
'CH': net_cmd_ch, # keepalive
'askchaa': net_cmd_askchaa, # ask for list lengths
'askchar2': net_cmd_askchar2, # ask for list of characters
'AN': net_cmd_an, # character list
'AE': net_cmd_ae, # evidence list
'AM': net_cmd_am, # music list
'RC': net_cmd_rc, # AO2 character list
'RM': net_cmd_rm, # AO2 music list
'RD': net_cmd_rd, # AO2 done request, charscheck etc.
'CC': net_cmd_cc, # select character
'MS': net_cmd_ms, # IC message
'CT': net_cmd_ct, # OOC message
'MC': net_cmd_mc, # play song
'RT': net_cmd_rt, # WT/CE buttons
'HP': net_cmd_hp, # penalties
'PE': net_cmd_pe, # add evidence
'DE': net_cmd_de, # delete evidence
'EE': net_cmd_ee, # edit evidence
'ZZ': net_cmd_zz, # call mod button
'opKICK': net_cmd_opKICK, # /kick with guard on
'opBAN': net_cmd_opBAN, # /ban with guard on
}

412
server/area_manager.py Normal file
View File

@ -0,0 +1,412 @@
# tsuserver3, an Attorney Online server
#
# Copyright (C) 2016 argoneus <argoneuscze@gmail.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import asyncio
import random
import time
import yaml
from server.exceptions import AreaError
from server.evidence import EvidenceList
from enum import Enum
class AreaManager:
class Area:
def __init__(self, area_id, server, name, background, bg_lock, evidence_mod = 'FFA', locking_allowed = False, iniswap_allowed = True, showname_changes_allowed = False, shouts_allowed = True, jukebox = False, abbreviation = '', non_int_pres_only = False):
self.iniswap_allowed = iniswap_allowed
self.clients = set()
self.invite_list = {}
self.id = area_id
self.name = name
self.background = background
self.bg_lock = bg_lock
self.server = server
self.music_looper = None
self.next_message_time = 0
self.hp_def = 10
self.hp_pro = 10
self.doc = 'No document.'
self.status = 'IDLE'
self.judgelog = []
self.current_music = ''
self.current_music_player = ''
self.current_music_player_ipid = -1
self.evi_list = EvidenceList()
self.is_recording = False
self.recorded_messages = []
self.evidence_mod = evidence_mod
self.locking_allowed = locking_allowed
self.showname_changes_allowed = showname_changes_allowed
self.shouts_allowed = shouts_allowed
self.abbreviation = abbreviation
self.cards = dict()
"""
#debug
self.evidence_list.append(Evidence("WOW", "desc", "1.png"))
self.evidence_list.append(Evidence("wewz", "desc2", "2.png"))
self.evidence_list.append(Evidence("weeeeeew", "desc3", "3.png"))
"""
self.is_locked = self.Locked.FREE
self.blankposting_allowed = True
self.non_int_pres_only = non_int_pres_only
self.jukebox = jukebox
self.jukebox_votes = []
self.jukebox_prev_char_id = -1
self.owners = []
class Locked(Enum):
FREE = 1,
SPECTATABLE = 2,
LOCKED = 3
def new_client(self, client):
self.clients.add(client)
self.server.area_manager.send_arup_players()
def remove_client(self, client):
self.clients.remove(client)
if len(self.clients) == 0:
self.change_status('IDLE')
def unlock(self):
self.is_locked = self.Locked.FREE
self.blankposting_allowed = True
self.invite_list = {}
self.server.area_manager.send_arup_lock()
self.send_host_message('This area is open now.')
def spectator(self):
self.is_locked = self.Locked.SPECTATABLE
for i in self.clients:
self.invite_list[i.id] = None
for i in self.owners:
self.invite_list[i.id] = None
self.server.area_manager.send_arup_lock()
self.send_host_message('This area is spectatable now.')
def lock(self):
self.is_locked = self.Locked.LOCKED
for i in self.clients:
self.invite_list[i.id] = None
for i in self.owners:
self.invite_list[i.id] = None
self.server.area_manager.send_arup_lock()
self.send_host_message('This area is locked now.')
def is_char_available(self, char_id):
return char_id not in [x.char_id for x in self.clients]
def get_rand_avail_char_id(self):
avail_set = set(range(len(self.server.char_list))) - set([x.char_id for x in self.clients])
if len(avail_set) == 0:
raise AreaError('No available characters.')
return random.choice(tuple(avail_set))
def send_command(self, cmd, *args):
for c in self.clients:
c.send_command(cmd, *args)
def send_owner_command(self, cmd, *args):
for c in self.owners:
if not c in self.clients:
c.send_command(cmd, *args)
def send_host_message(self, msg):
self.send_command('CT', self.server.config['hostname'], msg, '1')
self.send_owner_command('CT', '[' + self.abbreviation + ']' + self.server.config['hostname'], msg, '1')
def set_next_msg_delay(self, msg_length):
delay = min(3000, 100 + 60 * msg_length)
self.next_message_time = round(time.time() * 1000.0 + delay)
def is_iniswap(self, client, anim1, anim2, char):
if self.iniswap_allowed:
return False
if '..' in anim1 or '..' in anim2:
return True
for char_link in self.server.allowed_iniswaps:
if client.get_char_name() in char_link and char in char_link:
return False
return True
def add_jukebox_vote(self, client, music_name, length=-1, showname=''):
if not self.jukebox:
return
if length <= 0:
self.remove_jukebox_vote(client, False)
else:
self.remove_jukebox_vote(client, True)
self.jukebox_votes.append(self.JukeboxVote(client, music_name, length, showname))
client.send_host_message('Your song was added to the jukebox.')
if len(self.jukebox_votes) == 1:
self.start_jukebox()
def remove_jukebox_vote(self, client, silent):
if not self.jukebox:
return
for current_vote in self.jukebox_votes:
if current_vote.client.id == client.id:
self.jukebox_votes.remove(current_vote)
if not silent:
client.send_host_message('You removed your song from the jukebox.')
def get_jukebox_picked(self):
if not self.jukebox:
return
if len(self.jukebox_votes) == 0:
return None
elif len(self.jukebox_votes) == 1:
return self.jukebox_votes[0]
else:
weighted_votes = []
for current_vote in self.jukebox_votes:
i = 0
while i < current_vote.chance:
weighted_votes.append(current_vote)
i += 1
return random.choice(weighted_votes)
def start_jukebox(self):
# There is a probability that the jukebox feature has been turned off since then,
# we should check that.
# We also do a check if we were the last to play a song, just in case.
if not self.jukebox:
if self.current_music_player == 'The Jukebox' and self.current_music_player_ipid == 'has no IPID':
self.current_music = ''
return
vote_picked = self.get_jukebox_picked()
if vote_picked is None:
self.current_music = ''
return
if vote_picked.client.char_id != self.jukebox_prev_char_id or vote_picked.name != self.current_music or len(self.jukebox_votes) > 1:
self.jukebox_prev_char_id = vote_picked.client.char_id
if vote_picked.showname == '':
self.send_command('MC', vote_picked.name, vote_picked.client.char_id)
else:
self.send_command('MC', vote_picked.name, vote_picked.client.char_id, vote_picked.showname)
else:
self.send_command('MC', vote_picked.name, -1)
self.current_music_player = 'The Jukebox'
self.current_music_player_ipid = 'has no IPID'
self.current_music = vote_picked.name
for current_vote in self.jukebox_votes:
# Choosing the same song will get your votes down to 0, too.
# Don't want the same song twice in a row!
if current_vote.name == vote_picked.name:
current_vote.chance = 0
else:
current_vote.chance += 1
if self.music_looper:
self.music_looper.cancel()
self.music_looper = asyncio.get_event_loop().call_later(vote_picked.length, lambda: self.start_jukebox())
def play_music(self, name, cid, length=-1):
self.send_command('MC', name, cid)
if self.music_looper:
self.music_looper.cancel()
if length > 0:
self.music_looper = asyncio.get_event_loop().call_later(length,
lambda: self.play_music(name, -1, length))
def play_music_shownamed(self, name, cid, showname, length=-1):
self.send_command('MC', name, cid, showname)
if self.music_looper:
self.music_looper.cancel()
if length > 0:
self.music_looper = asyncio.get_event_loop().call_later(length,
lambda: self.play_music(name, -1, length))
def can_send_message(self, client):
if self.cannot_ic_interact(client):
client.send_host_message('This is a locked area - ask the CM to speak.')
return False
return (time.time() * 1000.0 - self.next_message_time) > 0
def cannot_ic_interact(self, client):
return self.is_locked != self.Locked.FREE and not client.is_mod and not client.id in self.invite_list
def change_hp(self, side, val):
if not 0 <= val <= 10:
raise AreaError('Invalid penalty value.')
if not 1 <= side <= 2:
raise AreaError('Invalid penalty side.')
if side == 1:
self.hp_def = val
elif side == 2:
self.hp_pro = val
self.send_command('HP', side, val)
def change_background(self, bg):
if bg.lower() not in (name.lower() for name in self.server.backgrounds):
raise AreaError('Invalid background name.')
self.background = bg
self.send_command('BN', self.background)
def change_status(self, value):
allowed_values = ('idle', 'rp', 'casing', 'looking-for-players', 'lfp', 'recess', 'gaming')
if value.lower() not in allowed_values:
raise AreaError('Invalid status. Possible values: {}'.format(', '.join(allowed_values)))
if value.lower() == 'lfp':
value = 'looking-for-players'
self.status = value.upper()
self.server.area_manager.send_arup_status()
def change_doc(self, doc='No document.'):
self.doc = doc
def add_to_judgelog(self, client, msg):
if len(self.judgelog) >= 10:
self.judgelog = self.judgelog[1:]
self.judgelog.append('{} ({}) {}.'.format(client.get_char_name(), client.get_ip(), msg))
def add_music_playing(self, client, name):
self.current_music_player = client.get_char_name()
self.current_music_player_ipid = client.ipid
self.current_music = name
def add_music_playing_shownamed(self, client, showname, name):
self.current_music_player = showname + " (" + client.get_char_name() + ")"
self.current_music_player_ipid = client.ipid
self.current_music = name
def get_evidence_list(self, client):
client.evi_list, evi_list = self.evi_list.create_evi_list(client)
return evi_list
def broadcast_evidence_list(self):
"""
LE#<name>&<desc>&<img>#<name>
"""
for client in self.clients:
client.send_command('LE', *self.get_evidence_list(client))
def get_cms(self):
msg = ''
for i in self.owners:
msg = msg + '[' + str(i.id) + '] ' + i.get_char_name() + ', '
if len(msg) > 2:
msg = msg[:-2]
return msg
class JukeboxVote:
def __init__(self, client, name, length, showname):
self.client = client
self.name = name
self.length = length
self.chance = 1
self.showname = showname
def __init__(self, server):
self.server = server
self.cur_id = 0
self.areas = []
self.load_areas()
def load_areas(self):
with open('config/areas.yaml', 'r') as chars:
areas = yaml.load(chars)
for item in areas:
if 'evidence_mod' not in item:
item['evidence_mod'] = 'FFA'
if 'locking_allowed' not in item:
item['locking_allowed'] = False
if 'iniswap_allowed' not in item:
item['iniswap_allowed'] = True
if 'showname_changes_allowed' not in item:
item['showname_changes_allowed'] = False
if 'shouts_allowed' not in item:
item['shouts_allowed'] = True
if 'jukebox' not in item:
item['jukebox'] = False
if 'noninterrupting_pres' not in item:
item['noninterrupting_pres'] = False
if 'abbreviation' not in item:
item['abbreviation'] = self.get_generated_abbreviation(item['area'])
self.areas.append(
self.Area(self.cur_id, self.server, item['area'], item['background'], item['bglock'], item['evidence_mod'], item['locking_allowed'], item['iniswap_allowed'], item['showname_changes_allowed'], item['shouts_allowed'], item['jukebox'], item['abbreviation'], item['noninterrupting_pres']))
self.cur_id += 1
def default_area(self):
return self.areas[0]
def get_area_by_name(self, name):
for area in self.areas:
if area.name == name:
return area
raise AreaError('Area not found.')
def get_area_by_id(self, num):
for area in self.areas:
if area.id == num:
return area
raise AreaError('Area not found.')
def get_generated_abbreviation(self, name):
if name.lower().startswith("courtroom"):
return "CR" + name.split()[-1]
elif name.lower().startswith("area"):
return "A" + name.split()[-1]
elif len(name.split()) > 1:
return "".join(item[0].upper() for item in name.split())
elif len(name) > 3:
return name[:3].upper()
else:
return name.upper()
def send_remote_command(self, area_ids, cmd, *args):
for a_id in area_ids:
self.get_area_by_id(a_id).send_command(cmd, *args)
self.get_area_by_id(a_id).send_owner_command(cmd, *args)
def send_arup_players(self):
players_list = [0]
for area in self.areas:
players_list.append(len(area.clients))
self.server.send_arup(players_list)
def send_arup_status(self):
status_list = [1]
for area in self.areas:
status_list.append(area.status)
self.server.send_arup(status_list)
def send_arup_cms(self):
cms_list = [2]
for area in self.areas:
cm = 'FREE'
if len(area.owners) > 0:
cm = area.get_cms()
cms_list.append(cm)
self.server.send_arup(cms_list)
def send_arup_lock(self):
lock_list = [3]
for area in self.areas:
lock_list.append(area.is_locked.name)
self.server.send_arup(lock_list)

54
server/ban_manager.py Normal file
View File

@ -0,0 +1,54 @@
# tsuserver3, an Attorney Online server
#
# Copyright (C) 2016 argoneus <argoneuscze@gmail.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import json
from server.exceptions import ServerError
class BanManager:
def __init__(self):
self.bans = []
self.load_banlist()
def load_banlist(self):
try:
with open('storage/banlist.json', 'r') as banlist_file:
self.bans = json.load(banlist_file)
except FileNotFoundError:
return
def write_banlist(self):
with open('storage/banlist.json', 'w') as banlist_file:
json.dump(self.bans, banlist_file)
def add_ban(self, ip):
if ip not in self.bans:
self.bans.append(ip)
else:
raise ServerError('This IPID is already banned.')
self.write_banlist()
def remove_ban(self, ip):
if ip in self.bans:
self.bans.remove(ip)
else:
raise ServerError('This IPID is not banned.')
self.write_banlist()
def is_banned(self, ipid):
return (ipid in self.bans)

457
server/client_manager.py Normal file
View File

@ -0,0 +1,457 @@
# tsuserver3, an Attorney Online server
#
# Copyright (C) 2016 argoneus <argoneuscze@gmail.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from server import fantacrypt
from server import logger
from server.exceptions import ClientError, AreaError
from enum import Enum
from server.constants import TargetType
from heapq import heappop, heappush
import time
import re
class ClientManager:
class Client:
def __init__(self, server, transport, user_id, ipid):
self.is_checked = False
self.transport = transport
self.hdid = ''
self.pm_mute = False
self.id = user_id
self.char_id = -1
self.area = server.area_manager.default_area()
self.server = server
self.name = ''
self.fake_name = ''
self.is_mod = False
self.is_dj = True
self.can_wtce = True
self.pos = ''
self.evi_list = []
self.disemvowel = False
self.shaken = False
self.charcurse = []
self.muted_global = False
self.muted_adverts = False
self.is_muted = False
self.is_ooc_muted = False
self.pm_mute = False
self.mod_call_time = 0
self.in_rp = False
self.ipid = ipid
self.websocket = None
# Pairing stuff
self.charid_pair = -1
self.offset_pair = 0
self.last_sprite = ''
self.flip = 0
self.claimed_folder = ''
# Casing stuff
self.casing_cm = False
self.casing_cases = ""
self.casing_def = False
self.casing_pro = False
self.casing_jud = False
self.casing_jur = False
self.casing_steno = False
self.case_call_time = 0
#flood-guard stuff
self.mus_counter = 0
self.mus_mute_time = 0
self.mus_change_time = [x * self.server.config['music_change_floodguard']['interval_length'] for x in range(self.server.config['music_change_floodguard']['times_per_interval'])]
self.wtce_counter = 0
self.wtce_mute_time = 0
self.wtce_time = [x * self.server.config['wtce_floodguard']['interval_length'] for x in range(self.server.config['wtce_floodguard']['times_per_interval'])]
def send_raw_message(self, msg):
if self.websocket:
self.websocket.send_text(msg.encode('utf-8'))
else:
self.transport.write(msg.encode('utf-8'))
def send_command(self, command, *args):
if args:
if command == 'MS':
for evi_num in range(len(self.evi_list)):
if self.evi_list[evi_num] == args[11]:
lst = list(args)
lst[11] = evi_num
args = tuple(lst)
break
self.send_raw_message('{}#{}#%'.format(command, '#'.join([str(x) for x in args])))
else:
self.send_raw_message('{}#%'.format(command))
def send_host_message(self, msg):
self.send_command('CT', self.server.config['hostname'], msg, '1')
def send_motd(self):
self.send_host_message('=== MOTD ===\r\n{}\r\n============='.format(self.server.config['motd']))
def send_player_count(self):
self.send_host_message('{}/{} players online.'.format(
self.server.get_player_count(),
self.server.config['playerlimit']))
def is_valid_name(self, name):
name_ws = name.replace(' ', '')
if not name_ws or name_ws.isdigit():
return False
for client in self.server.client_manager.clients:
print(client.name == name)
if client.name == name:
return False
return True
def disconnect(self):
self.transport.close()
def change_character(self, char_id, force=False):
if not self.server.is_valid_char_id(char_id):
raise ClientError('Invalid Character ID.')
if len(self.charcurse) > 0:
if not char_id in self.charcurse:
raise ClientError('Character not available.')
force = True
if not self.area.is_char_available(char_id):
if force:
for client in self.area.clients:
if client.char_id == char_id:
client.char_select()
else:
raise ClientError('Character not available.')
old_char = self.get_char_name()
self.char_id = char_id
self.pos = ''
self.send_command('PV', self.id, 'CID', self.char_id)
self.area.send_command('CharsCheck', *self.get_available_char_list())
logger.log_server('[{}]Changed character from {} to {}.'
.format(self.area.abbreviation, old_char, self.get_char_name()), self)
def change_music_cd(self):
if self.is_mod or self in self.area.owners:
return 0
if self.mus_mute_time:
if time.time() - self.mus_mute_time < self.server.config['music_change_floodguard']['mute_length']:
return self.server.config['music_change_floodguard']['mute_length'] - (time.time() - self.mus_mute_time)
else:
self.mus_mute_time = 0
times_per_interval = self.server.config['music_change_floodguard']['times_per_interval']
interval_length = self.server.config['music_change_floodguard']['interval_length']
if time.time() - self.mus_change_time[(self.mus_counter - times_per_interval + 1) % times_per_interval] < interval_length:
self.mus_mute_time = time.time()
return self.server.config['music_change_floodguard']['mute_length']
self.mus_counter = (self.mus_counter + 1) % times_per_interval
self.mus_change_time[self.mus_counter] = time.time()
return 0
def wtce_mute(self):
if self.is_mod or self in self.area.owners:
return 0
if self.wtce_mute_time:
if time.time() - self.wtce_mute_time < self.server.config['wtce_floodguard']['mute_length']:
return self.server.config['wtce_floodguard']['mute_length'] - (time.time() - self.wtce_mute_time)
else:
self.wtce_mute_time = 0
times_per_interval = self.server.config['wtce_floodguard']['times_per_interval']
interval_length = self.server.config['wtce_floodguard']['interval_length']
if time.time() - self.wtce_time[(self.wtce_counter - times_per_interval + 1) % times_per_interval] < interval_length:
self.wtce_mute_time = time.time()
return self.server.config['music_change_floodguard']['mute_length']
self.wtce_counter = (self.wtce_counter + 1) % times_per_interval
self.wtce_time[self.wtce_counter] = time.time()
return 0
def reload_character(self):
try:
self.change_character(self.char_id, True)
except ClientError:
raise
def change_area(self, area):
if self.area == area:
raise ClientError('User already in specified area.')
if area.is_locked == area.Locked.LOCKED and not self.is_mod and not self.id in area.invite_list:
raise ClientError("That area is locked!")
if area.is_locked == area.Locked.SPECTATABLE and not self.is_mod and not self.id in area.invite_list:
self.send_host_message('This area is spectatable, but not free - you will be unable to send messages ICly unless invited.')
if self.area.jukebox:
self.area.remove_jukebox_vote(self, True)
old_area = self.area
if not area.is_char_available(self.char_id):
try:
new_char_id = area.get_rand_avail_char_id()
except AreaError:
raise ClientError('No available characters in that area.')
self.change_character(new_char_id)
self.send_host_message('Character taken, switched to {}.'.format(self.get_char_name()))
self.area.remove_client(self)
self.area = area
area.new_client(self)
self.send_host_message('Changed area to {}.[{}]'.format(area.name, self.area.status))
logger.log_server(
'[{}]Changed area from {} ({}) to {} ({}).'.format(self.get_char_name(), old_area.name, old_area.id,
self.area.name, self.area.id), self)
self.area.send_command('CharsCheck', *self.get_available_char_list())
self.send_command('HP', 1, self.area.hp_def)
self.send_command('HP', 2, self.area.hp_pro)
self.send_command('BN', self.area.background)
self.send_command('LE', *self.area.get_evidence_list(self))
def send_area_list(self):
msg = '=== Areas ==='
for i, area in enumerate(self.server.area_manager.areas):
owner = 'FREE'
if len(area.owners) > 0:
owner = 'CM: {}'.format(area.get_cms())
lock = {area.Locked.FREE: '', area.Locked.SPECTATABLE: '[SPECTATABLE]', area.Locked.LOCKED: '[LOCKED]'}
msg += '\r\nArea {}: {} (users: {}) [{}][{}]{}'.format(area.abbreviation, area.name, len(area.clients), area.status, owner, lock[area.is_locked])
if self.area == area:
msg += ' [*]'
self.send_host_message(msg)
def get_area_info(self, area_id, mods):
info = '\r\n'
try:
area = self.server.area_manager.get_area_by_id(area_id)
except AreaError:
raise
info += '=== {} ==='.format(area.name)
info += '\r\n'
lock = {area.Locked.FREE: '', area.Locked.SPECTATABLE: '[SPECTATABLE]', area.Locked.LOCKED: '[LOCKED]'}
info += '[{}]: [{} users][{}]{}'.format(area.abbreviation, len(area.clients), area.status, lock[area.is_locked])
sorted_clients = []
for client in area.clients:
if (not mods) or client.is_mod:
sorted_clients.append(client)
for owner in area.owners:
if not (mods or owner in area.clients):
sorted_clients.append(owner)
if not sorted_clients:
return ''
sorted_clients = sorted(sorted_clients, key=lambda x: x.get_char_name())
for c in sorted_clients:
info += '\r\n'
if c in area.owners:
if not c in area.clients:
info += '[RCM]'
else:
info +='[CM]'
info += '[{}] {}'.format(c.id, c.get_char_name())
if self.is_mod:
info += ' ({})'.format(c.ipid)
info += ': {}'.format(c.name)
return info
def send_area_info(self, area_id, mods):
#if area_id is -1 then return all areas. If mods is True then return only mods
info = ''
if area_id == -1:
# all areas info
cnt = 0
info = '\n== Area List =='
for i in range(len(self.server.area_manager.areas)):
if len(self.server.area_manager.areas[i].clients) > 0 or len(self.server.area_manager.areas[i].owners) > 0:
cnt += len(self.server.area_manager.areas[i].clients)
info += '{}'.format(self.get_area_info(i, mods))
info = 'Current online: {}'.format(cnt) + info
else:
try:
info = 'People in this area: {}'.format(len(self.server.area_manager.areas[area_id].clients)) + self.get_area_info(area_id, mods)
except AreaError:
raise
self.send_host_message(info)
def send_area_hdid(self, area_id):
try:
info = self.get_area_hdid(area_id)
except AreaError:
raise
self.send_host_message(info)
def send_all_area_hdid(self):
info = '== HDID List =='
for i in range (len(self.server.area_manager.areas)):
if len(self.server.area_manager.areas[i].clients) > 0:
info += '\r\n{}'.format(self.get_area_hdid(i))
self.send_host_message(info)
def send_all_area_ip(self):
info = '== IP List =='
for i in range (len(self.server.area_manager.areas)):
if len(self.server.area_manager.areas[i].clients) > 0:
info += '\r\n{}'.format(self.get_area_ip(i))
self.send_host_message(info)
def send_done(self):
self.send_command('CharsCheck', *self.get_available_char_list())
self.send_command('HP', 1, self.area.hp_def)
self.send_command('HP', 2, self.area.hp_pro)
self.send_command('BN', self.area.background)
self.send_command('LE', *self.area.get_evidence_list(self))
self.send_command('MM', 1)
self.server.area_manager.send_arup_players()
self.server.area_manager.send_arup_status()
self.server.area_manager.send_arup_cms()
self.server.area_manager.send_arup_lock()
self.send_command('DONE')
def char_select(self):
self.char_id = -1
self.send_done()
def get_available_char_list(self):
if len(self.charcurse) > 0:
avail_char_ids = set(range(len(self.server.char_list))) and set(self.charcurse)
else:
avail_char_ids = set(range(len(self.server.char_list))) - set([x.char_id for x in self.area.clients])
char_list = [-1] * len(self.server.char_list)
for x in avail_char_ids:
char_list[x] = 0
return char_list
def auth_mod(self, password):
if self.is_mod:
raise ClientError('Already logged in.')
if password == self.server.config['modpass']:
self.is_mod = True
else:
raise ClientError('Invalid password.')
def get_ip(self):
return self.ipid
def get_char_name(self):
if self.char_id == -1:
return 'CHAR_SELECT'
return self.server.char_list[self.char_id]
def change_position(self, pos=''):
if pos not in ('', 'def', 'pro', 'hld', 'hlp', 'jud', 'wit', 'jur', 'sea'):
raise ClientError('Invalid position. Possible values: def, pro, hld, hlp, jud, wit, jur, sea.')
self.pos = pos
def set_mod_call_delay(self):
self.mod_call_time = round(time.time() * 1000.0 + 30000)
def can_call_mod(self):
return (time.time() * 1000.0 - self.mod_call_time) > 0
def set_case_call_delay(self):
self.case_call_time = round(time.time() * 1000.0 + 60000)
def can_call_case(self):
return (time.time() * 1000.0 - self.case_call_time) > 0
def disemvowel_message(self, message):
message = re.sub("[aeiou]", "", message, flags=re.IGNORECASE)
return re.sub(r"\s+", " ", message)
def shake_message(self, message):
import random
parts = message.split()
random.shuffle(parts)
return ' '.join(parts)
def __init__(self, server):
self.clients = set()
self.server = server
self.cur_id = [i for i in range(self.server.config['playerlimit'])]
self.clients_list = []
def new_client(self, transport):
c = self.Client(self.server, transport, heappop(self.cur_id), self.server.get_ipid(transport.get_extra_info('peername')[0]))
self.clients.add(c)
return c
def remove_client(self, client):
if client.area.jukebox:
client.area.remove_jukebox_vote(client, True)
for a in self.server.area_manager.areas:
if client in a.owners:
a.owners.remove(client)
client.server.area_manager.send_arup_cms()
if len(a.owners) == 0:
if a.is_locked != a.Locked.FREE:
a.unlock()
heappush(self.cur_id, client.id)
self.clients.remove(client)
def get_targets(self, client, key, value, local = False):
#possible keys: ip, OOC, id, cname, ipid, hdid
areas = None
if local:
areas = [client.area]
else:
areas = client.server.area_manager.areas
targets = []
if key == TargetType.ALL:
for nkey in range(6):
targets += self.get_targets(client, nkey, value, local)
for area in areas:
for client in area.clients:
if key == TargetType.IP:
if value.lower().startswith(client.get_ip().lower()):
targets.append(client)
elif key == TargetType.OOC_NAME:
if value.lower().startswith(client.name.lower()) and client.name:
targets.append(client)
elif key == TargetType.CHAR_NAME:
if value.lower().startswith(client.get_char_name().lower()):
targets.append(client)
elif key == TargetType.ID:
if client.id == value:
targets.append(client)
elif key == TargetType.IPID:
if client.ipid == value:
targets.append(client)
return targets
def get_muted_clients(self):
clients = []
for client in self.clients:
if client.is_muted:
clients.append(client)
return clients
def get_ooc_muted_clients(self):
clients = []
for client in self.clients:
if client.is_ooc_muted:
clients.append(client)
return clients

1255
server/commands.py Normal file

File diff suppressed because it is too large Load Diff

11
server/constants.py Normal file
View File

@ -0,0 +1,11 @@
from enum import Enum
class TargetType(Enum):
#possible keys: ip, OOC, id, cname, ipid, hdid
IP = 0
OOC_NAME = 1
ID = 2
CHAR_NAME = 3
IPID = 4
HDID = 5
ALL = 6

79
server/districtclient.py Normal file
View File

@ -0,0 +1,79 @@
# tsuserver3, an Attorney Online server
#
# Copyright (C) 2016 argoneus <argoneuscze@gmail.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import asyncio
from server import logger
class DistrictClient:
def __init__(self, server):
self.server = server
self.reader = None
self.writer = None
self.message_queue = []
async def connect(self):
loop = asyncio.get_event_loop()
while True:
try:
self.reader, self.writer = await asyncio.open_connection(self.server.config['district_ip'],
self.server.config['district_port'], loop=loop)
await self.handle_connection()
except (ConnectionRefusedError, TimeoutError):
pass
except (ConnectionResetError, asyncio.IncompleteReadError):
self.writer = None
self.reader = None
finally:
logger.log_debug("Couldn't connect to the district, retrying in 30 seconds.")
await asyncio.sleep(30)
async def handle_connection(self):
logger.log_debug('District connected.')
self.send_raw_message('AUTH#{}'.format(self.server.config['district_password']))
while True:
data = await self.reader.readuntil(b'\r\n')
if not data:
return
raw_msg = data.decode()[:-2]
logger.log_debug('[DISTRICT][INC][RAW]{}'.format(raw_msg))
cmd, *args = raw_msg.split('#')
if cmd == 'GLOBAL':
glob_name = '{}[{}:{}][{}]'.format('<dollar>G', args[1], args[2], args[3])
if args[0] == '1':
glob_name += '[M]'
self.server.send_all_cmd_pred('CT', glob_name, args[4], pred=lambda x: not x.muted_global)
elif cmd == 'NEED':
need_msg = '=== Cross Advert ===\r\n{} at {} in {} [{}] needs {}\r\n====================' \
.format(args[1], args[0], args[2], args[3], args[4])
self.server.send_all_cmd_pred('CT', '{}'.format(self.server.config['hostname']), need_msg, '1',
pred=lambda x: not x.muted_adverts)
async def write_queue(self):
while self.message_queue:
msg = self.message_queue.pop(0)
try:
self.writer.write(msg)
await self.writer.drain()
except ConnectionResetError:
return
def send_raw_message(self, msg):
if not self.writer:
return
self.message_queue.append('{}\r\n'.format(msg).encode())
asyncio.ensure_future(self.write_queue(), loop=asyncio.get_event_loop())

100
server/evidence.py Normal file
View File

@ -0,0 +1,100 @@
class EvidenceList:
limit = 35
class Evidence:
def __init__(self, name, desc, image, pos):
self.name = name
self.desc = desc
self.image = image
self.public = False
self.pos = pos
def set_name(self, name):
self.name = name
def set_desc(self, desc):
self.desc = desc
def set_image(self, image):
self.image = image
def to_string(self):
sequence = (self.name, self.desc, self.image)
return '&'.join(sequence)
def __init__(self):
self.evidences = []
self.poses = {'def':['def', 'hld'],
'pro':['pro', 'hlp'],
'wit':['wit', 'sea'],
'sea':['sea', 'wit'],
'hlp':['hlp', 'pro'],
'hld':['hld', 'def'],
'jud':['jud', 'jur'],
'jur':['jur', 'jud'],
'all':['hlp', 'hld', 'wit', 'jud', 'pro', 'def', 'jur', 'sea', ''],
'pos':[]}
def login(self, client):
if client.area.evidence_mod == 'FFA':
pass
if client.area.evidence_mod == 'Mods':
if not client in client.area.owners:
return False
if client.area.evidence_mod == 'CM':
if not client in client.area.owners and not client.is_mod:
return False
if client.area.evidence_mod == 'HiddenCM':
if not client in client.area.owners and not client.is_mod:
return False
return True
def correct_format(self, client, desc):
if client.area.evidence_mod != 'HiddenCM':
return True
else:
#correct format: <owner = pos>\ndesc
if desc[:9] == '<owner = ' and desc[9:12] in self.poses and desc[12:14] == '>\n':
return True
return False
def add_evidence(self, client, name, description, image, pos = 'all'):
if self.login(client):
if client.area.evidence_mod == 'HiddenCM':
pos = 'pos'
if len(self.evidences) >= self.limit:
client.send_host_message('You can\'t have more than {} evidence items at a time.'.format(self.limit))
else:
self.evidences.append(self.Evidence(name, description, image, pos))
def evidence_swap(self, client, id1, id2):
if self.login(client):
self.evidences[id1], self.evidences[id2] = self.evidences[id2], self.evidences[id1]
def create_evi_list(self, client):
evi_list = []
nums_list = [0]
for i in range(len(self.evidences)):
if client.area.evidence_mod == 'HiddenCM' and self.login(client):
nums_list.append(i + 1)
evi = self.evidences[i]
evi_list.append(self.Evidence(evi.name, '<owner = {}>\n{}'.format(evi.pos, evi.desc), evi.image, evi.pos).to_string())
elif client.pos in self.poses[self.evidences[i].pos]:
nums_list.append(i + 1)
evi_list.append(self.evidences[i].to_string())
return nums_list, evi_list
def del_evidence(self, client, id):
if self.login(client):
self.evidences.pop(id)
def edit_evidence(self, client, id, arg):
if self.login(client):
if client.area.evidence_mod == 'HiddenCM' and self.correct_format(client, arg[1]):
self.evidences[id] = self.Evidence(arg[0], arg[1][14:], arg[2], arg[1][9:12])
return
if client.area.evidence_mod == 'HiddenCM':
client.send_host_message('You entered a wrong pos.')
return
self.evidences[id] = self.Evidence(arg[0], arg[1], arg[2], arg[3])

32
server/exceptions.py Normal file
View File

@ -0,0 +1,32 @@
# tsuserver3, an Attorney Online server
#
# Copyright (C) 2016 argoneus <argoneuscze@gmail.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
class ClientError(Exception):
pass
class AreaError(Exception):
pass
class ArgumentError(Exception):
pass
class ServerError(Exception):
pass

45
server/fantacrypt.py Normal file
View File

@ -0,0 +1,45 @@
# tsuserver3, an Attorney Online server
#
# Copyright (C) 2016 argoneus <argoneuscze@gmail.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
# fantacrypt was a mistake, just hardcoding some numbers is good enough
import binascii
CRYPT_CONST_1 = 53761
CRYPT_CONST_2 = 32618
CRYPT_KEY = 5
def fanta_decrypt(data):
data_bytes = [int(data[x:x + 2], 16) for x in range(0, len(data), 2)]
key = CRYPT_KEY
ret = ''
for byte in data_bytes:
val = byte ^ ((key & 0xffff) >> 8)
ret += chr(val)
key = ((byte + key) * CRYPT_CONST_1) + CRYPT_CONST_2
return ret
def fanta_encrypt(data):
key = CRYPT_KEY
ret = ''
for char in data:
val = ord(char) ^ ((key & 0xffff) >> 8)
ret += binascii.hexlify(val.to_bytes(1, byteorder='big')).decode().upper()
key = ((val + key) * CRYPT_CONST_1) + CRYPT_CONST_2
return ret

78
server/logger.py Normal file
View File

@ -0,0 +1,78 @@
# tsuserver3, an Attorney Online server
#
# Copyright (C) 2016 argoneus <argoneuscze@gmail.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import logging
import time
def setup_logger(debug):
logging.Formatter.converter = time.gmtime
debug_formatter = logging.Formatter('[%(asctime)s UTC]%(message)s')
srv_formatter = logging.Formatter('[%(asctime)s UTC]%(message)s')
mod_formatter = logging.Formatter('[%(asctime)s UTC]%(message)s')
debug_log = logging.getLogger('debug')
debug_log.setLevel(logging.DEBUG)
debug_handler = logging.FileHandler('logs/debug.log', encoding='utf-8')
debug_handler.setLevel(logging.DEBUG)
debug_handler.setFormatter(debug_formatter)
debug_log.addHandler(debug_handler)
if not debug:
debug_log.disabled = True
server_log = logging.getLogger('server')
server_log.setLevel(logging.INFO)
server_handler = logging.FileHandler('logs/server.log', encoding='utf-8')
server_handler.setLevel(logging.INFO)
server_handler.setFormatter(srv_formatter)
server_log.addHandler(server_handler)
mod_log = logging.getLogger('mod')
mod_log.setLevel(logging.INFO)
mod_handler = logging.FileHandler('logs/mod.log', encoding='utf-8')
mod_handler.setLevel(logging.INFO)
mod_handler.setFormatter(mod_formatter)
mod_log.addHandler(mod_handler)
def log_debug(msg, client=None):
msg = parse_client_info(client) + msg
logging.getLogger('debug').debug(msg)
def log_server(msg, client=None):
msg = parse_client_info(client) + msg
logging.getLogger('server').info(msg)
def log_mod(msg, client=None):
msg = parse_client_info(client) + msg
logging.getLogger('mod').info(msg)
def parse_client_info(client):
if client is None:
return ''
info = client.get_ip()
if client.is_mod:
return '[{:<15}][{:<3}][{}][MOD]'.format(info, client.id, client.name)
return '[{:<15}][{:<3}][{}]'.format(info, client.id, client.name)

View File

@ -0,0 +1,89 @@
# tsuserver3, an Attorney Online server
#
# Copyright (C) 2016 argoneus <argoneuscze@gmail.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import asyncio
import time
from server import logger
class MasterServerClient:
def __init__(self, server):
self.server = server
self.reader = None
self.writer = None
async def connect(self):
loop = asyncio.get_event_loop()
while True:
try:
self.reader, self.writer = await asyncio.open_connection(self.server.config['masterserver_ip'],
self.server.config['masterserver_port'],
loop=loop)
await self.handle_connection()
except (ConnectionRefusedError, TimeoutError):
pass
except (ConnectionResetError, asyncio.IncompleteReadError):
self.writer = None
self.reader = None
finally:
logger.log_debug("Couldn't connect to the master server, retrying in 30 seconds.")
print("Couldn't connect to the master server, retrying in 30 seconds.")
await asyncio.sleep(30)
async def handle_connection(self):
logger.log_debug('Master server connected.')
await self.send_server_info()
fl = False
lastping = time.time() - 20
while True:
self.reader.feed_data(b'END')
full_data = await self.reader.readuntil(b'END')
full_data = full_data[:-3]
if len(full_data) > 0:
data_list = list(full_data.split(b'#%'))[:-1]
for data in data_list:
raw_msg = data.decode()
cmd, *args = raw_msg.split('#')
if cmd != 'CHECK' and cmd != 'PONG':
logger.log_debug('[MASTERSERVER][INC][RAW]{}'.format(raw_msg))
elif cmd == 'CHECK':
await self.send_raw_message('PING#%')
elif cmd == 'PONG':
fl = False
elif cmd == 'NOSERV':
await self.send_server_info()
if time.time() - lastping > 5:
if fl:
return
lastping = time.time()
fl = True
await self.send_raw_message('PING#%')
await asyncio.sleep(1)
async def send_server_info(self):
cfg = self.server.config
msg = 'SCC#{}#{}#{}#{}#%'.format(cfg['port'], cfg['masterserver_name'], cfg['masterserver_description'],
self.server.software)
await self.send_raw_message(msg)
async def send_raw_message(self, msg):
try:
self.writer.write(msg.encode())
await self.writer.drain()
except ConnectionResetError:
return

305
server/tsuserver.py Normal file
View File

@ -0,0 +1,305 @@
# tsuserver3, an Attorney Online server
#
# Copyright (C) 2016 argoneus <argoneuscze@gmail.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import asyncio
import yaml
import json
from server import logger
from server.aoprotocol import AOProtocol
from server.area_manager import AreaManager
from server.ban_manager import BanManager
from server.client_manager import ClientManager
from server.districtclient import DistrictClient
from server.exceptions import ServerError
from server.masterserverclient import MasterServerClient
class TsuServer3:
def __init__(self):
self.config = None
self.allowed_iniswaps = None
self.load_config()
self.load_iniswaps()
self.client_manager = ClientManager(self)
self.area_manager = AreaManager(self)
self.ban_manager = BanManager()
self.software = 'tsuserver3'
self.version = 'tsuserver3dev'
self.release = 3
self.major_version = 1
self.minor_version = 1
self.ipid_list = {}
self.hdid_list = {}
self.char_list = None
self.char_pages_ao1 = None
self.music_list = None
self.music_list_ao2 = None
self.music_pages_ao1 = None
self.backgrounds = None
self.load_characters()
self.load_music()
self.load_backgrounds()
self.load_ids()
self.district_client = None
self.ms_client = None
self.rp_mode = False
logger.setup_logger(debug=self.config['debug'])
def start(self):
loop = asyncio.get_event_loop()
bound_ip = '0.0.0.0'
if self.config['local']:
bound_ip = '127.0.0.1'
ao_server_crt = loop.create_server(lambda: AOProtocol(self), bound_ip, self.config['port'])
ao_server = loop.run_until_complete(ao_server_crt)
if self.config['use_district']:
self.district_client = DistrictClient(self)
asyncio.ensure_future(self.district_client.connect(), loop=loop)
if self.config['use_masterserver']:
self.ms_client = MasterServerClient(self)
asyncio.ensure_future(self.ms_client.connect(), loop=loop)
logger.log_debug('Server started.')
try:
loop.run_forever()
except KeyboardInterrupt:
pass
logger.log_debug('Server shutting down.')
ao_server.close()
loop.run_until_complete(ao_server.wait_closed())
loop.close()
def get_version_string(self):
return str(self.release) + '.' + str(self.major_version) + '.' + str(self.minor_version)
def new_client(self, transport):
c = self.client_manager.new_client(transport)
if self.rp_mode:
c.in_rp = True
c.server = self
c.area = self.area_manager.default_area()
c.area.new_client(c)
return c
def remove_client(self, client):
client.area.remove_client(client)
self.client_manager.remove_client(client)
def get_player_count(self):
return len(self.client_manager.clients)
def load_config(self):
with open('config/config.yaml', 'r', encoding = 'utf-8') as cfg:
self.config = yaml.load(cfg)
self.config['motd'] = self.config['motd'].replace('\\n', ' \n')
if 'music_change_floodguard' not in self.config:
self.config['music_change_floodguard'] = {'times_per_interval': 1, 'interval_length': 0, 'mute_length': 0}
if 'wtce_floodguard' not in self.config:
self.config['wtce_floodguard'] = {'times_per_interval': 1, 'interval_length': 0, 'mute_length': 0}
def load_characters(self):
with open('config/characters.yaml', 'r', encoding = 'utf-8') as chars:
self.char_list = yaml.load(chars)
self.build_char_pages_ao1()
def load_music(self):
with open('config/music.yaml', 'r', encoding = 'utf-8') as music:
self.music_list = yaml.load(music)
self.build_music_pages_ao1()
self.build_music_list_ao2()
def load_ids(self):
self.ipid_list = {}
self.hdid_list = {}
#load ipids
try:
with open('storage/ip_ids.json', 'r', encoding = 'utf-8') as whole_list:
self.ipid_list = json.loads(whole_list.read())
except:
logger.log_debug('Failed to load ip_ids.json from ./storage. If ip_ids.json is exist then remove it.')
#load hdids
try:
with open('storage/hd_ids.json', 'r', encoding = 'utf-8') as whole_list:
self.hdid_list = json.loads(whole_list.read())
except:
logger.log_debug('Failed to load hd_ids.json from ./storage. If hd_ids.json is exist then remove it.')
def dump_ipids(self):
with open('storage/ip_ids.json', 'w') as whole_list:
json.dump(self.ipid_list, whole_list)
def dump_hdids(self):
with open('storage/hd_ids.json', 'w') as whole_list:
json.dump(self.hdid_list, whole_list)
def get_ipid(self, ip):
if not (ip in self.ipid_list):
self.ipid_list[ip] = len(self.ipid_list)
self.dump_ipids()
return self.ipid_list[ip]
def load_backgrounds(self):
with open('config/backgrounds.yaml', 'r', encoding = 'utf-8') as bgs:
self.backgrounds = yaml.load(bgs)
def load_iniswaps(self):
try:
with open('config/iniswaps.yaml', 'r', encoding = 'utf-8') as iniswaps:
self.allowed_iniswaps = yaml.load(iniswaps)
except:
logger.log_debug('cannot find iniswaps.yaml')
def build_char_pages_ao1(self):
self.char_pages_ao1 = [self.char_list[x:x + 10] for x in range(0, len(self.char_list), 10)]
for i in range(len(self.char_list)):
self.char_pages_ao1[i // 10][i % 10] = '{}#{}&&0&&&0&'.format(i, self.char_list[i])
def build_music_pages_ao1(self):
self.music_pages_ao1 = []
index = 0
# add areas first
for area in self.area_manager.areas:
self.music_pages_ao1.append('{}#{}'.format(index, area.name))
index += 1
# then add music
for item in self.music_list:
self.music_pages_ao1.append('{}#{}'.format(index, item['category']))
index += 1
for song in item['songs']:
self.music_pages_ao1.append('{}#{}'.format(index, song['name']))
index += 1
self.music_pages_ao1 = [self.music_pages_ao1[x:x + 10] for x in range(0, len(self.music_pages_ao1), 10)]
def build_music_list_ao2(self):
self.music_list_ao2 = []
# add areas first
for area in self.area_manager.areas:
self.music_list_ao2.append(area.name)
# then add music
for item in self.music_list:
self.music_list_ao2.append(item['category'])
for song in item['songs']:
self.music_list_ao2.append(song['name'])
def is_valid_char_id(self, char_id):
return len(self.char_list) > char_id >= 0
def get_char_id_by_name(self, name):
for i, ch in enumerate(self.char_list):
if ch.lower() == name.lower():
return i
raise ServerError('Character not found.')
def get_song_data(self, music):
for item in self.music_list:
if item['category'] == music:
return item['category'], -1
for song in item['songs']:
if song['name'] == music:
try:
return song['name'], song['length']
except KeyError:
return song['name'], -1
raise ServerError('Music not found.')
def send_all_cmd_pred(self, cmd, *args, pred=lambda x: True):
for client in self.client_manager.clients:
if pred(client):
client.send_command(cmd, *args)
def broadcast_global(self, client, msg, as_mod=False):
char_name = client.get_char_name()
ooc_name = '{}[{}][{}]'.format('<dollar>G', client.area.abbreviation, char_name)
if as_mod:
ooc_name += '[M]'
self.send_all_cmd_pred('CT', ooc_name, msg, pred=lambda x: not x.muted_global)
if self.config['use_district']:
self.district_client.send_raw_message(
'GLOBAL#{}#{}#{}#{}'.format(int(as_mod), client.area.id, char_name, msg))
def send_modchat(self, client, msg):
name = client.name
ooc_name = '{}[{}][{}]'.format('<dollar>M', client.area.abbreviation, name)
self.send_all_cmd_pred('CT', ooc_name, msg, pred=lambda x: x.is_mod)
if self.config['use_district']:
self.district_client.send_raw_message(
'MODCHAT#{}#{}#{}'.format(client.area.id, char_name, msg))
def broadcast_need(self, client, msg):
char_name = client.get_char_name()
area_name = client.area.name
area_id = client.area.abbreviation
self.send_all_cmd_pred('CT', '{}'.format(self.config['hostname']),
['=== Advert ===\r\n{} in {} [{}] needs {}\r\n==============='
.format(char_name, area_name, area_id, msg), '1'], pred=lambda x: not x.muted_adverts)
if self.config['use_district']:
self.district_client.send_raw_message('NEED#{}#{}#{}#{}'.format(char_name, area_name, area_id, msg))
def send_arup(self, args):
""" Updates the area properties on the Case Café Custom Client.
Playercount:
ARUP#0#<area1_p: int>#<area2_p: int>#...
Status:
ARUP#1##<area1_s: string>##<area2_s: string>#...
CM:
ARUP#2##<area1_cm: string>##<area2_cm: string>#...
Lockedness:
ARUP#3##<area1_l: string>##<area2_l: string>#...
"""
if len(args) < 2:
# An argument count smaller than 2 means we only got the identifier of ARUP.
return
if args[0] not in (0,1,2,3):
return
if args[0] == 0:
for part_arg in args[1:]:
try:
sanitised = int(part_arg)
except:
return
elif args[0] in (1, 2, 3):
for part_arg in args[1:]:
try:
sanitised = str(part_arg)
except:
return
self.send_all_cmd_pred('ARUP', *args, pred=lambda x: True)
def refresh(self):
with open('config/config.yaml', 'r') as cfg:
self.config['motd'] = yaml.load(cfg)['motd'].replace('\\n', ' \n')
with open('config/characters.yaml', 'r') as chars:
self.char_list = yaml.load(chars)
with open('config/music.yaml', 'r') as music:
self.music_list = yaml.load(music)
self.build_music_pages_ao1()
self.build_music_list_ao2()
with open('config/backgrounds.yaml', 'r') as bgs:
self.backgrounds = yaml.load(bgs)

215
server/websocket.py Normal file
View File

@ -0,0 +1,215 @@
# tsuserver3, an Attorney Online server
#
# Copyright (C) 2017 argoneus <argoneuscze@gmail.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
# Partly authored by Johan Hanssen Seferidis (MIT license):
# https://github.com/Pithikos/python-websocket-server
import asyncio
import re
import struct
from base64 import b64encode
from hashlib import sha1
from server import logger
class Bitmasks:
FIN = 0x80
OPCODE = 0x0f
MASKED = 0x80
PAYLOAD_LEN = 0x7f
PAYLOAD_LEN_EXT16 = 0x7e
PAYLOAD_LEN_EXT64 = 0x7f
class Opcode:
CONTINUATION = 0x0
TEXT = 0x1
BINARY = 0x2
CLOSE_CONN = 0x8
PING = 0x9
PONG = 0xA
class WebSocket:
"""
State data for clients that are connected via a WebSocket that wraps
over a conventional TCP connection.
"""
def __init__(self, client, protocol):
self.client = client
self.transport = client.transport
self.protocol = protocol
self.keep_alive = True
self.handshake_done = False
self.valid = False
def handle(self, data):
if not self.handshake_done:
return self.handshake(data)
return self.parse(data)
def parse(self, data):
b1, b2 = 0, 0
if len(data) >= 2:
b1, b2 = data[0], data[1]
fin = b1 & Bitmasks.FIN
opcode = b1 & Bitmasks.OPCODE
masked = b2 & Bitmasks.MASKED
payload_length = b2 & Bitmasks.PAYLOAD_LEN
if not b1:
# Connection closed
self.keep_alive = 0
return
if opcode == Opcode.CLOSE_CONN:
# Connection close requested
self.keep_alive = 0
return
if not masked:
# Client was not masked (spec violation)
logger.log_debug("ws: client was not masked.", self.client)
self.keep_alive = 0
print(data)
return
if opcode == Opcode.CONTINUATION:
# No continuation frames supported
logger.log_debug("ws: client tried to send continuation frame.", self.client)
return
elif opcode == Opcode.BINARY:
# No binary frames supported
logger.log_debug("ws: client tried to send binary frame.", self.client)
return
elif opcode == Opcode.TEXT:
def opcode_handler(s, msg):
return msg
elif opcode == Opcode.PING:
opcode_handler = self.send_pong
elif opcode == Opcode.PONG:
opcode_handler = lambda s, msg: None
else:
# Unknown opcode
logger.log_debug("ws: unknown opcode!", self.client)
self.keep_alive = 0
return
mask_offset = 2
if payload_length == 126:
payload_length = struct.unpack(">H", data[2:4])[0]
mask_offset = 4
elif payload_length == 127:
payload_length = struct.unpack(">Q", data[2:10])[0]
mask_offset = 10
masks = data[mask_offset:mask_offset + 4]
decoded = ""
for char in data[mask_offset + 4:payload_length + mask_offset + 4]:
char ^= masks[len(decoded) % 4]
decoded += chr(char)
return opcode_handler(self, decoded)
def send_message(self, message):
self.send_text(message)
def send_pong(self, message):
self.send_text(message, Opcode.PONG)
def send_text(self, message, opcode=Opcode.TEXT):
"""
Important: Fragmented (continuation) messages are not supported since
their usage cases are limited - when we don't know the payload length.
"""
# Validate message
if isinstance(message, bytes):
message = message.decode("utf-8")
elif isinstance(message, str):
pass
else:
raise TypeError("Message must be either str or bytes")
header = bytearray()
payload = message.encode("utf-8")
payload_length = len(payload)
# Normal payload
if payload_length <= 125:
header.append(Bitmasks.FIN | opcode)
header.append(payload_length)
# Extended payload
elif payload_length >= 126 and payload_length <= 65535:
header.append(Bitmasks.FIN | opcode)
header.append(Bitmasks.PAYLOAD_LEN_EXT16)
header.extend(struct.pack(">H", payload_length))
# Huge extended payload
elif payload_length < (1 << 64):
header.append(Bitmasks.FIN | opcode)
header.append(Bitmasks.PAYLOAD_LEN_EXT64)
header.extend(struct.pack(">Q", payload_length))
else:
raise Exception("Message is too big")
self.transport.write(header + payload)
def handshake(self, data):
try:
message = data[0:1024].decode().strip()
except UnicodeDecodeError:
return False
upgrade = re.search('\nupgrade[\s]*:[\s]*websocket', message.lower())
if not upgrade:
self.keep_alive = False
return False
key = re.search('\n[sS]ec-[wW]eb[sS]ocket-[kK]ey[\s]*:[\s]*(.*)\r\n', message)
if key:
key = key.group(1)
else:
logger.log_debug("Client tried to connect but was missing a key", self.client)
self.keep_alive = False
return False
response = self.make_handshake_response(key)
print(response.encode())
self.transport.write(response.encode())
self.handshake_done = True
self.valid = True
return True
def make_handshake_response(self, key):
return \
'HTTP/1.1 101 Switching Protocols\r\n'\
'Upgrade: websocket\r\n' \
'Connection: Upgrade\r\n' \
'Sec-WebSocket-Accept: %s\r\n' \
'\r\n' % self.calculate_response_key(key)
def calculate_response_key(self, key):
GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
hash = sha1(key.encode() + GUID.encode())
response_key = b64encode(hash.digest()).strip()
return response_key.decode('ASCII')
def finish(self):
self.protocol.connection_lost(self)

View File

@ -1,98 +1,72 @@
#include "aoapplication.h"
#include "file_functions.h"
#include <QTextStream>
#include <QStringList>
#include <QVector>
#include <QDebug>
#include <QColor>
QString AOApplication::read_config(QString searchline)
{
QString return_value = "";
QFile config_file(get_base_path() + "config.ini");
if (!config_file.open(QIODevice::ReadOnly))
return return_value;
QTextStream in(&config_file);
while(!in.atEnd())
{
QString f_line = in.readLine().trimmed();
if (!f_line.startsWith(searchline))
continue;
QStringList line_elements = f_line.split("=");
if (line_elements.at(0).trimmed() != searchline)
continue;
if (line_elements.size() < 2)
continue;
return_value = line_elements.at(1).trimmed();
break;
}
config_file.close();
return return_value;
}
#include "text_file_functions.h"
QString AOApplication::read_theme()
{
QString result = read_config("theme");
if (result == "")
return "default";
else
return result;
}
QString AOApplication::get_ooc_name()
{
return read_config("ooc_name");
QString result = configini->value("theme", "default").value<QString>();
return result;
}
int AOApplication::read_blip_rate()
{
QString result = read_config("blip_rate");
int result = configini->value("blip_rate", 1).toInt();
return result;
}
//note: the empty string converted to int will return 0
if (result.toInt() <= 0)
return 1;
else
return result.toInt();
QString AOApplication::get_ooc_name()
{
QString result = configini->value("ooc_name").value<QString>();
return result;
}
int AOApplication::get_default_music()
{
QString f_result = read_config("default_music");
if (f_result == "")
return 50;
else return f_result.toInt();
int result = configini->value("default_music", 50).toInt();
return result;
}
int AOApplication::get_default_sfx()
{
QString f_result = read_config("default_sfx");
if (f_result == "")
return 50;
else return f_result.toInt();
int result = configini->value("default_sfx", 50).toInt();
return result;
}
int AOApplication::get_default_blip()
{
QString f_result = read_config("default_blip");
int result = configini->value("default_blip", 50).toInt();
return result;
}
if (f_result == "")
return 50;
else return f_result.toInt();
int AOApplication::get_max_log_size()
{
int result = configini->value("log_maximum", 200).toInt();
return result;
}
bool AOApplication::get_log_goes_downwards()
{
QString result = configini->value("log_goes_downwards", "false").value<QString>();
return result.startsWith("true");
}
bool AOApplication::get_showname_enabled_by_default()
{
QString result = configini->value("show_custom_shownames", "false").value<QString>();
return result.startsWith("true");
}
QString AOApplication::get_default_username()
{
QString result = configini->value("default_username", "").value<QString>();
if (result.isEmpty())
return get_ooc_name();
else
return result;
}
QString AOApplication::get_audio_output_device()
{
QString result = configini->value("default_audio_device", "default").value<QString>();
return result;
}
QStringList AOApplication::get_call_words()
@ -174,40 +148,13 @@ QVector<server_type> AOApplication::read_serverlist_txt()
QString AOApplication::read_design_ini(QString p_identifier, QString p_design_path)
{
QFile design_ini;
design_ini.setFileName(p_design_path);
if (!design_ini.open(QIODevice::ReadOnly))
{
return "";
QSettings settings(p_design_path, QSettings::IniFormat);
QVariant value = settings.value(p_identifier);
if (value.type() == QVariant::StringList) {
return value.toStringList().join(",");
} else {
return value.toString();
}
QTextStream in(&design_ini);
QString result = "";
while (!in.atEnd())
{
QString f_line = in.readLine().trimmed();
if (!f_line.startsWith(p_identifier))
continue;
QStringList line_elements = f_line.split("=");
if (line_elements.at(0).trimmed() != p_identifier)
continue;
if (line_elements.size() < 2)
continue;
result = line_elements.at(1).trimmed();
break;
}
design_ini.close();
return result;
}
QPoint AOApplication::get_button_spacing(QString p_identifier, QString p_file)
@ -319,6 +266,34 @@ QColor AOApplication::get_color(QString p_identifier, QString p_file)
return return_color;
}
QColor AOApplication::get_chat_color(QString p_identifier, QString p_chat)
{
p_identifier = p_identifier.prepend("c");
QString design_ini_path = get_base_path() + "misc/" + p_chat + "/config.ini";
QString default_path = get_base_path() + "misc/default/config.ini";
QString f_result = read_design_ini(p_identifier, design_ini_path);
QColor return_color(255, 255, 255);
if (f_result == "")
{
f_result = read_design_ini(p_identifier, default_path);
if (f_result == "")
return return_color;
}
QStringList color_list = f_result.split(",");
if (color_list.size() < 3)
return return_color;
return_color.setRed(color_list.at(0).toInt());
return_color.setGreen(color_list.at(1).toInt());
return_color.setBlue(color_list.at(2).toInt());
return return_color;
}
QString AOApplication::get_sfx(QString p_identifier)
{
QString design_ini_path = get_theme_path() + "courtroom_sounds.ini";
@ -342,59 +317,18 @@ QString AOApplication::get_sfx(QString p_identifier)
//returns whatever is to the right of "search_line =" within target_tag and terminator_tag, trimmed
//returns the empty string if the search line couldnt be found
QString AOApplication::read_char_ini(QString p_char, QString p_search_line, QString target_tag, QString terminator_tag)
QString AOApplication::read_char_ini(QString p_char, QString p_search_line, QString target_tag)
{
QString char_ini_path = get_character_path(p_char) + "char.ini";
QFile char_ini;
char_ini.setFileName(char_ini_path);
if (!char_ini.open(QIODevice::ReadOnly))
return "";
QTextStream in(&char_ini);
bool tag_found = false;
while(!in.atEnd())
{
QString line = in.readLine();
if (QString::compare(line, terminator_tag, Qt::CaseInsensitive) == 0)
break;
if (line.startsWith(target_tag, Qt::CaseInsensitive))
{
tag_found = true;
continue;
}
if (!line.startsWith(p_search_line, Qt::CaseInsensitive))
continue;
QStringList line_elements = line.split("=");
if (QString::compare(line_elements.at(0).trimmed(), p_search_line, Qt::CaseInsensitive) != 0)
continue;
if (line_elements.size() < 2)
continue;
if (tag_found)
{
char_ini.close();
return line_elements.at(1).trimmed();
}
}
char_ini.close();
return "";
QSettings settings(get_character_path(p_char) + "char.ini", QSettings::IniFormat);
settings.beginGroup(target_tag);
QString value = settings.value(p_search_line).toString();
settings.endGroup();
return value;
}
QString AOApplication::get_char_name(QString p_char)
{
QString f_result = read_char_ini(p_char, "name", "[Options]", "[Time]");
QString f_result = read_char_ini(p_char, "name", "Options");
if (f_result == "")
return p_char;
@ -403,8 +337,11 @@ QString AOApplication::get_char_name(QString p_char)
QString AOApplication::get_showname(QString p_char)
{
QString f_result = read_char_ini(p_char, "showname", "[Options]", "[Time]");
QString f_result = read_char_ini(p_char, "showname", "Options");
QString f_needed = read_char_ini(p_char, "needs_showname", "Options");
if (f_needed.startsWith("false"))
return "";
if (f_result == "")
return p_char;
else return f_result;
@ -412,7 +349,7 @@ QString AOApplication::get_showname(QString p_char)
QString AOApplication::get_char_side(QString p_char)
{
QString f_result = read_char_ini(p_char, "side", "[Options]", "[Time]");
QString f_result = read_char_ini(p_char, "side", "Options");
if (f_result == "")
return "wit";
@ -421,7 +358,7 @@ QString AOApplication::get_char_side(QString p_char)
QString AOApplication::get_gender(QString p_char)
{
QString f_result = read_char_ini(p_char, "gender", "[Options]", "[Time]");
QString f_result = read_char_ini(p_char, "gender", "Options");
if (f_result == "")
return "male";
@ -430,7 +367,7 @@ QString AOApplication::get_gender(QString p_char)
QString AOApplication::get_chat(QString p_char)
{
QString f_result = read_char_ini(p_char, "chat", "[Options]", "[Time]");
QString f_result = read_char_ini(p_char, "chat", "Options");
//handling the correct order of chat is a bit complicated, we let the caller do it
return f_result.toLower();
@ -438,14 +375,14 @@ QString AOApplication::get_chat(QString p_char)
QString AOApplication::get_char_shouts(QString p_char)
{
QString f_result = read_char_ini(p_char, "shouts", "[Options]", "[Time]");
QString f_result = read_char_ini(p_char, "shouts", "Options");
return f_result.toLower();
}
int AOApplication::get_preanim_duration(QString p_char, QString p_emote)
{
QString f_result = read_char_ini(p_char, p_emote, "[Time]", "[Emotions]");
QString f_result = read_char_ini(p_char, p_emote, "Time");
if (f_result == "")
return -1;
@ -454,7 +391,7 @@ int AOApplication::get_preanim_duration(QString p_char, QString p_emote)
int AOApplication::get_ao2_preanim_duration(QString p_char, QString p_emote)
{
QString f_result = read_char_ini(p_char, "%" + p_emote, "[Time]", "[Emotions]");
QString f_result = read_char_ini(p_char, "%" + p_emote, "Time");
if (f_result == "")
return -1;
@ -463,7 +400,7 @@ int AOApplication::get_ao2_preanim_duration(QString p_char, QString p_emote)
int AOApplication::get_emote_number(QString p_char)
{
QString f_result = read_char_ini(p_char, "number", "[Emotions]", "[SoundN]");
QString f_result = read_char_ini(p_char, "number", "Emotions");
if (f_result == "")
return 0;
@ -472,7 +409,7 @@ int AOApplication::get_emote_number(QString p_char)
QString AOApplication::get_emote_comment(QString p_char, int p_emote)
{
QString f_result = read_char_ini(p_char, QString::number(p_emote + 1), "[Emotions]", "[SoundN]");
QString f_result = read_char_ini(p_char, QString::number(p_emote + 1), "Emotions");
QStringList result_contents = f_result.split("#");
@ -486,7 +423,7 @@ QString AOApplication::get_emote_comment(QString p_char, int p_emote)
QString AOApplication::get_pre_emote(QString p_char, int p_emote)
{
QString f_result = read_char_ini(p_char, QString::number(p_emote + 1), "[Emotions]", "[SoundN]");
QString f_result = read_char_ini(p_char, QString::number(p_emote + 1), "Emotions");
QStringList result_contents = f_result.split("#");
@ -500,7 +437,7 @@ QString AOApplication::get_pre_emote(QString p_char, int p_emote)
QString AOApplication::get_emote(QString p_char, int p_emote)
{
QString f_result = read_char_ini(p_char, QString::number(p_emote + 1), "[Emotions]", "[SoundN]");
QString f_result = read_char_ini(p_char, QString::number(p_emote + 1), "Emotions");
QStringList result_contents = f_result.split("#");
@ -514,7 +451,7 @@ QString AOApplication::get_emote(QString p_char, int p_emote)
int AOApplication::get_emote_mod(QString p_char, int p_emote)
{
QString f_result = read_char_ini(p_char, QString::number(p_emote + 1), "[Emotions]", "[SoundN]");
QString f_result = read_char_ini(p_char, QString::number(p_emote + 1), "Emotions");
QStringList result_contents = f_result.split("#");
@ -528,7 +465,7 @@ int AOApplication::get_emote_mod(QString p_char, int p_emote)
int AOApplication::get_desk_mod(QString p_char, int p_emote)
{
QString f_result = read_char_ini(p_char, QString::number(p_emote + 1), "[Emotions]", "[SoundN]");
QString f_result = read_char_ini(p_char, QString::number(p_emote + 1), "Emotions");
QStringList result_contents = f_result.split("#");
@ -544,7 +481,7 @@ int AOApplication::get_desk_mod(QString p_char, int p_emote)
QString AOApplication::get_sfx_name(QString p_char, int p_emote)
{
QString f_result = read_char_ini(p_char, QString::number(p_emote + 1), "[SoundN]", "[SoundT]");
QString f_result = read_char_ini(p_char, QString::number(p_emote + 1), "SoundN");
if (f_result == "")
return "1";
@ -553,7 +490,7 @@ QString AOApplication::get_sfx_name(QString p_char, int p_emote)
int AOApplication::get_sfx_delay(QString p_char, int p_emote)
{
QString f_result = read_char_ini(p_char, QString::number(p_emote + 1), "[SoundT]", "[TextDelay]");
QString f_result = read_char_ini(p_char, QString::number(p_emote + 1), "SoundT");
if (f_result == "")
return 1;
@ -562,28 +499,78 @@ int AOApplication::get_sfx_delay(QString p_char, int p_emote)
int AOApplication::get_text_delay(QString p_char, QString p_emote)
{
QString f_result = read_char_ini(p_char, p_emote, "[TextDelay]", "END_OF_FILE");
QString f_result = read_char_ini(p_char, p_emote, "TextDelay");
if (f_result == "")
return -1;
else return f_result.toInt();
}
QString AOApplication::get_custom_realization(QString p_char)
{
QString f_result = read_char_ini(p_char, "realization", "Options");
if (f_result == "")
return get_sfx("realization");
else return f_result;
}
bool AOApplication::get_blank_blip()
{
QString f_result = read_config("blank_blip");
return f_result.startsWith("true");
QString result = configini->value("blank_blip", "false").value<QString>();
return result.startsWith("true");
}
bool AOApplication::is_discord_enabled()
{
QString f_result = read_config("discord");
return !f_result.startsWith("false");
QString result = configini->value("discord", "true").value<QString>();
return result.startsWith("true");
}
bool AOApplication::ic_scroll_down_enabled()
bool AOApplication::get_casing_enabled()
{
QString f_result = read_config("ic_scroll_down");
return f_result.startsWith("true");
QString result = configini->value("casing_enabled", "false").value<QString>();
return result.startsWith("true");
}
bool AOApplication::get_casing_defence_enabled()
{
QString result = configini->value("casing_defence_enabled", "false").value<QString>();
return result.startsWith("true");
}
bool AOApplication::get_casing_prosecution_enabled()
{
QString result = configini->value("casing_prosecution_enabled", "false").value<QString>();
return result.startsWith("true");
}
bool AOApplication::get_casing_judge_enabled()
{
QString result = configini->value("casing_judge_enabled", "false").value<QString>();
return result.startsWith("true");
}
bool AOApplication::get_casing_juror_enabled()
{
QString result = configini->value("casing_juror_enabled", "false").value<QString>();
return result.startsWith("true");
}
bool AOApplication::get_casing_steno_enabled()
{
QString result = configini->value("casing_steno_enabled", "false").value<QString>();
return result.startsWith("true");
}
bool AOApplication::get_casing_cm_enabled()
{
QString result = configini->value("casing_cm_enabled", "false").value<QString>();
return result.startsWith("true");
}
QString AOApplication::get_casing_can_host_cases()
{
QString result = configini->value("casing_can_host_casees", "Turnabout Check Your Settings").value<QString>();
return result;
}

13
text_file_functions.h Normal file
View File

@ -0,0 +1,13 @@
#ifndef TEXT_FILE_FUNCTIONS_H
#define TEXT_FILE_FUNCTIONS_H
#include "aoapplication.h"
#include "file_functions.h"
#include <QTextStream>
#include <QStringList>
#include <QVector>
#include <QDebug>
#include <QColor>
#include <QSettings>
#endif // TEXT_FILE_FUNCTIONS_H