A virtual currency betting bot for Twitch chat. https://ddark.net/better
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
better/src/main.cpp

1831 lines
80 KiB

#pragma warning (disable: 4996) // This function or variable may be unsafe (strcpy, sprintf, ...)
#pragma warning (disable: 4200) // nonstandard extension used: zero-sized array in struct/union
#pragma warning (disable: 4809) // switch statement has redundant 'default' label
#include <cstdlib>
#include <cstdio>
#include <cstring>
#include <ctime>
#include <winsock2.h>
#include <d3d11.h>
#include <tchar.h>
#include <imgui.h>
#include <imgui_internal.h>
#include <imgui_impl_dx11.h>
#include <imgui_impl_win32.h>
#include <implot.h>
#include <binn.h>
#include <stb_sprintf.h>
#include <fa.h>
#include "better.h"
#include "better_alloc.h"
#define STBDS_REALLOC(c,p,s) BETTER_REALLOC(p, s, CODE_LOCATION)
#define STBDS_FREE(c,p) BETTER_FREE(p)
#define STB_DS_IMPLEMENTATION
#include <stb_ds.h>
#undef STB_DS_IMPLEMENTATION
#include "better_func.h"
#include "better_App.h"
#include "better_irc.h"
#include "better_bets.h"
#include "better_imgui_utils.h"
#include "better_RingBuffer.h"
#if BETTER_DEBUG
extern i32 spoof_message_file_index;
extern f32 spoof_interval;
extern i32 spoof_chunk_size;
i32 frame_sleep = 0;
#endif
// Data
static ID3D11Device* g_pd3dDevice = NULL;
static ID3D11DeviceContext* g_pd3dDeviceContext = NULL;
static IDXGISwapChain* g_pSwapChain = NULL;
static ID3D11RenderTargetView* g_mainRenderTargetView = NULL;
// Forward declarations of helper functions
bool CreateDeviceD3D(HWND hWnd);
void CleanupDeviceD3D();
void CreateRenderTarget();
void CleanupRenderTarget();
LRESULT WINAPI WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam);
App app;
extern int tree_comp(UserInfo* k1, UserInfo* k2);
#if BETTER_DEBUG
i32 main(i32, char**)
#else
INT WinMain(HINSTANCE, HINSTANCE, PSTR, INT)
#endif
{
HINSTANCE hInstance = GetModuleHandle(NULL);
srand((u32)time(NULL));
stbds_rand_seed((size_t)rand());
stbsp_set_separators(' ', '.');
ost_init(&app.leaderboard_tree, (ost_compare_fn)tree_comp, tree_alloc, tree_free);
app.log_buffer.init(LOG_BUFFER_MAX);
app.chat_buffer.init(CHAT_BUFFER_MAX);
app.write_queue.init(1);
app.privmsg_queue.init(20);
app.read_queue.init(20);
app.point_feedback_queue.init(100);
for (int i = 0; i < 2; ++i)
{
BetTable bt = {};
BetTable_init(&bt);
arrput(app.bet_options, bt);
arraddnptr(app.bet_history, 1);
}
{
i32 bufsize = 64;
app.base_dir = (WCHAR*) BETTER_ALLOC(sizeof(WCHAR)*bufsize, "base_dir");
WCHAR* ptr = app.base_dir + GetModuleFileNameW(hInstance, app.base_dir, bufsize);
while (GetLastError() == ERROR_INSUFFICIENT_BUFFER)
{
bufsize *= 2;
app.base_dir = (WCHAR*) BETTER_REALLOC(app.base_dir, sizeof(WCHAR)*bufsize, "base_dir");
ptr = app.base_dir + GetModuleFileNameW(hInstance, app.base_dir, bufsize);
}
app.base_dir_len = wcslen(app.base_dir);
while (*ptr != L'\\') --ptr;
ptr[1] = '\0';
}
add_log(&app, LOGLEVEL_DEBUG, "Program starting up.");
load_settings_from_disk(&app);
load_leaderboard_from_disk(&app);
LARGE_INTEGER qpc_frequency;
QueryPerformanceFrequency(&qpc_frequency);
LARGE_INTEGER qpc_ticks;
QueryPerformanceCounter(&qpc_ticks);
f64 now = (f64)qpc_ticks.QuadPart / (f64)qpc_frequency.QuadPart;
if (!irc_init(&app)) return 1;
ImGui_ImplWin32_EnableDpiAwareness();
// Load resources
HRSRC rsrc_font_default = FindResource(hInstance, L"font_default", RT_RCDATA);
assert(rsrc_font_default);
HGLOBAL h_font_default = LoadResource(hInstance, rsrc_font_default);
assert(h_font_default);
app.data_font_normal = LockResource(h_font_default);
assert(app.data_font_normal);
app.data_size_font_normal = SizeofResource(hInstance, rsrc_font_default);
assert(app.data_size_font_normal);
HRSRC rsrc_font_mono = FindResource(hInstance, L"font_mono", RT_RCDATA);
assert(rsrc_font_mono);
HGLOBAL h_font_mono = LoadResource(hInstance, rsrc_font_mono);
assert(h_font_mono);
app.data_font_mono = LockResource(h_font_mono);
assert(app.data_font_mono);
app.data_size_font_mono = SizeofResource(hInstance, rsrc_font_mono);
assert(app.data_size_font_mono);
HICON icon_handle = LoadIconA(hInstance, "icon");
add_log(&app, LOGLEVEL_DEBUG, "Creating window.");
// Create window
WNDCLASSEX wc = { sizeof(WNDCLASSEX), CS_CLASSDC, WndProc, 0L, 0L, hInstance, icon_handle, NULL, NULL, NULL, _T("Better"), NULL };
::RegisterClassEx(&wc);
app.main_wnd = ::CreateWindow(wc.lpszClassName, _T("Better"), WS_OVERLAPPEDWINDOW, 100, 100, 1000, 800, NULL, NULL, wc.hInstance, NULL);
// Initialize Direct3D
if (!CreateDeviceD3D(app.main_wnd))
{
CleanupDeviceD3D();
::UnregisterClass(wc.lpszClassName, wc.hInstance);
return 1;
}
// Show the window
::ShowWindow(app.main_wnd, SW_SHOWDEFAULT);
::UpdateWindow(app.main_wnd);
add_log(&app, LOGLEVEL_DEBUG, "Creating imgui context.");
// Setup Dear ImGui context
IMGUI_CHECKVERSION();
ImGui::CreateContext();
ImGuiIO& io = ImGui::GetIO();
if (app.settings.imgui_ini)
ImGui::LoadIniSettingsFromMemory(app.settings.imgui_ini);
io.IniFilename = NULL;
io.ConfigFlags |= ImGuiConfigFlags_DockingEnable;
io.ConfigInputTextCursorBlink = false;
ImPlot::CreateContext();
app.default_font_config.FontDataOwnedByAtlas = false;
app.fa_font_config.MergeMode = true;
app.fa_font_config.PixelSnapH = true;
// Setup Dear ImGui style
update_imgui_style(&app);
// Setup Platform/Renderer bindings
ImGui_ImplWin32_Init(app.main_wnd);
ImGui_ImplDX11_Init(g_pd3dDevice, g_pd3dDeviceContext);
bool show_demo_window = false;
ImVec4 clear_color = ImVec4(0.45f, 0.55f, 0.60f, 1.00f);
if (app.settings.auto_connect)
irc_connect(&app);
i32 consecutive_frames_without_messages = 0;
MSG msg;
ZeroMemory(&msg, sizeof(msg));
while (true)
{
#if BETTER_DEBUG
if (frame_sleep) Sleep(frame_sleep);
#endif
if (consecutive_frames_without_messages > MIN_FRAMES_BEFORE_WAIT &&
!imgui_any_mouse_buttons_held(io) &&
bets_status(&app) == BETS_STATUS_CLOSED)
WaitMessage();
else ++consecutive_frames_without_messages;
while (::PeekMessage(&msg, NULL, 0U, 0U, PM_REMOVE))
{
::TranslateMessage(&msg);
::DispatchMessage(&msg);
consecutive_frames_without_messages = 0;
if (msg.message == WM_QUIT) break;
}
if (msg.message == WM_QUIT) break;
if (io.WantSaveIniSettings)
{
copy_imgui_ini(&app);
io.WantSaveIniSettings = false;
}
f64 last_frame_time = now;
QueryPerformanceCounter(&qpc_ticks);
now = (f64)qpc_ticks.QuadPart / (f64)qpc_frequency.QuadPart;
f64 dt = now - last_frame_time;
// Handle new messages
while (app.read_queue.count > 0)
{
IrcMessage msg = app.read_queue.pop_front();
irc_handle_message(&app, &msg);
}
// Update timer
if (app.timer_left > - (f32)app.settings.coyote_time)
{
app.timer_left -= (f32)dt;
if (app.timer_left <= - (f32)app.settings.coyote_time)
close_bets(&app);
}
// Update data used in GUI
u32 total_number_of_bets = 0;
f64 grand_total_bets = 0;
static f64* option_totals = NULL;
arrsetlen(option_totals, 0);
for (int i = 0; i < arrlen(app.bet_options); ++i)
{
total_number_of_bets += (u32)shlen(app.bet_options[i].bets);
arrput(option_totals, BetTable_get_point_sum(&app.bet_options[i]));
grand_total_bets += option_totals[arrlen(option_totals)-1];
}
assert(arrlen(option_totals) == arrlen(app.bet_options));
char points_name_cap[POINTS_NAME_MAX];
strcpy(points_name_cap, app.settings.points_name);
points_name_cap[0] = toupper(points_name_cap[0]);
i32 timeline_current_index = -1;
if (bets_status(&app) != BETS_STATUS_CLOSED)
{
timeline_current_index = (i32)(TIMELINE_SAMPLE_COUNT*(1.0-(f32)(app.timer_left+app.settings.coyote_time)/(f32)(app.settings.timer_setting+app.settings.coyote_time)));
for (int i = 0; i < arrlen(app.bet_history); ++i)
app.bet_history[i][timeline_current_index] = option_totals[i];
}
const ImVec4* current_color_set = (ImVec4*) app.color_theme_is_dark? COLOR_SET_DARK : COLOR_SET_LIGHT;
RECT rect;
GetClientRect(app.main_wnd, &rect);
i32 display_w = rect.right - rect.left,
display_h = rect.bottom - rect.top;
// Check point feedback queue
if (app.privmsg_ready &&
app.point_feedback_queue.count != 0 &&
app.privmsg_queue.count == 0)
{
char* buf = (char*) BETTER_ALLOC(SEND_BUFLEN + 1, "feedback_queue_write_buffer");
stbsp_sprintf(buf, "PRIVMSG #%s :%s: ", app.settings.channel, points_name_cap);
size_t used_chars = strlen(buf);
char single[SEND_BUFLEN+1];
while (app.point_feedback_queue.count > 0)
{
UserInfo* user = app.point_feedback_queue.get(0);
f64 points_used = 0;
for (int i = 0; i < arrlen(app.bet_options); ++i)
{
auto bet = shgeti(app.bet_options[i].bets, user->name);
if (bet != -1)
points_used += app.bet_options[i].bets[bet].value;
}
if (points_used > 0)
snprintf(single, SEND_BUFLEN+1, "%s: %.0f/%llu, ", user->name, points_used, user->points);
else
snprintf(single, SEND_BUFLEN+1, "%s: %llu, ", user->name, user->points);
size_t n = strlen(single);
if (used_chars + n > SEND_BUFLEN) break;
app.point_feedback_queue.pop_front();
strcpy(buf + used_chars, single);
used_chars += n;
}
strcpy(buf + used_chars - 2, "\r\n");
irc_queue_write(&app, buf, true);
}
// Rebuild fonts if needed
static bool should_rebuild_fonts = true;
if (should_rebuild_fonts)
{
rebuild_fonts(&app, io);
should_rebuild_fonts = false;
}
// Start the Dear ImGui frame
ImGui_ImplDX11_NewFrame();
ImGui_ImplWin32_NewFrame();
ImGui::NewFrame();
/////////////////
// MAIN WINDOW //
/////////////////
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0,0));
ImGui::SetNextWindowPos(ImVec2(0,0));
ImGui::SetNextWindowSize(ImVec2((f32)display_w, (f32)display_h));
ImGuiWindowFlags wnd_flags = ImGuiWindowFlags_NoDecoration
| ImGuiWindowFlags_NoMove
| ImGuiWindowFlags_NoScrollWithMouse
| ImGuiWindowFlags_NoDocking
| ImGuiWindowFlags_NoBringToFrontOnFocus
| ImGuiWindowFlags_MenuBar;
if (ImGui::Begin("Main", NULL, wnd_flags))
{
ImGui::PushStyleVar(
ImGuiStyleVar_WindowPadding,
ImVec2(8,8)); // default WindowPadding
if (ImGui::BeginMenuBar())
{
if (ImGui::BeginMenu("View"))
{
ImGui::MenuItem(WINDOW_NAME_BETS, NULL, &app.settings.show_window_bets);
ImGui::MenuItem(WINDOW_NAME_CHAT, NULL, &app.settings.show_window_chat);
#if BETTER_DEBUG
ImGui::MenuItem(WINDOW_NAME_DEBUG, NULL, &app.settings.show_window_debug);
#endif
ImGui::MenuItem(WINDOW_NAME_POINTS, NULL, &app.settings.show_window_points);
ImGui::MenuItem(WINDOW_NAME_LOG, NULL, &app.settings.show_window_log);
ImGui::MenuItem(WINDOW_NAME_SETTINGS, NULL, &app.settings.show_window_settings);
ImGui::MenuItem(WINDOW_NAME_STATS, NULL, &app.settings.show_window_statistics);
ImGui::EndMenu();
}
if (ImGui::BeginMenu("Help"))
{
ImGui::TextUnformatted("Better " BETTER_VERSION_STR "\n\nMade by ddarknut.\nContact: mail@ddark.net");
if (imgui_clickable_text("ddark.net/better"))
open_url("https://ddark.net/better");
ImGui::TextUnformatted("\nThird party software:");
ImGui::Text("Binn %s", binn_version());
ImGui::SameLine();
if (imgui_clickable_text("github.com/liteserver/binn"))
open_url("https://github.com/liteserver/binn");
ImGui::TextUnformatted("cregex");
ImGui::SameLine();
if (imgui_clickable_text("github.com/jserv/cregex"))
open_url("https://github.com/jserv/cregex");
ImGui::Text("Dear ImGui %s", ImGui::GetVersion());
ImGui::SameLine();
if (imgui_clickable_text("github.com/ocornut/imgui"))
open_url("https://github.com/ocornut/imgui");
ImGui::TextUnformatted("Fira font family");
ImGui::SameLine();
if (imgui_clickable_text("github.com/bBoxType/FiraSans"))
open_url("https://github.com/bBoxType/FiraSans");
ImGui::TextUnformatted("Fork Awesome 1.2");
ImGui::SameLine();
if (imgui_clickable_text("github.com/ForkAwesome/Fork-Awesome"))
open_url("https://github.com/ForkAwesome/Fork-Awesome");
ImGui::TextUnformatted("ImGuiFontStudio");
ImGui::SameLine();
if (imgui_clickable_text("github.com/aiekick/ImGuiFontStudio"))
open_url("https://github.com/aiekick/ImGuiFontStudio");
ImGui::Text("ImPlot %s", IMPLOT_VERSION);
ImGui::SameLine();
if (imgui_clickable_text("github.com/epezent/implot"))
open_url("https://github.com/epezent/implot");
ImGui::TextUnformatted("stb");
ImGui::SameLine();
if (imgui_clickable_text("github.com/nothings/stb"))
open_url("https://github.com/nothings/stb");
ImGui::EndMenu();
}
const char* irc_status_text;
if (app.joined_channel)
irc_status_text = "Connected to Twitch.";
else if (dns_thread_running(&app))
irc_status_text = "Waiting for DNS request...";
else if (app.sock != INVALID_SOCKET)
irc_status_text = "Connecting...";
else
irc_status_text = "Disconnected from Twitch.";
const char* bets_status_text;
if (bets_status(&app) != BETS_STATUS_CLOSED)
bets_status_text = "Bets are open.";
else
bets_status_text = "Bets are closed.";
char text[256];
stbsp_sprintf(text, "%s %s", irc_status_text, bets_status_text);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(10,0));
ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 100.0f);
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.5f, 0.5f, 0.5f, 0.2f));
if (ImGui::BeginChild("Status",
ImVec2(ImGui::GetContentRegionAvail().x-8, ImGui::GetFrameHeight()),
false,
ImGuiWindowFlags_NoScrollbar
| ImGuiWindowFlags_NoScrollWithMouse
| ImGuiWindowFlags_AlwaysUseWindowPadding
))
{
ImGui::AlignTextToFramePadding();
ImGui::Text(text);
if (app.unread_error != -1)
{
ImGui::SameLine();
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, current_color_set[LOGLEVEL_USERERROR]);
if (imgui_clickable_text("Log contains new errors."))
{
app.settings.show_window_log = true;
app.should_focus_log_window = true;
}
ImGui::PopStyleColor();
}
}
ImGui::EndChild();
ImGui::PopStyleColor(1);
ImGui::PopStyleVar(2);
ImGui::EndMenuBar();
}
ImGuiID main_dockspace_id = ImGui::GetID("MyDockSpace");
ImVec2 avail = ImGui::GetContentRegionAvail();
if (ImGui::DockBuilderGetNode(main_dockspace_id) == NULL)
{
ImGui::DockBuilderRemoveNode(main_dockspace_id); // Clear out existing layout
ImGui::DockBuilderAddNode(main_dockspace_id, ImGuiDockNodeFlags_DockSpace); // Add empty node
ImGui::DockBuilderSetNodeSize(main_dockspace_id, ImVec2(avail.x, avail.y));
ImGuiID dock_id_current = main_dockspace_id;
ImGuiID dock_id_log = ImGui::DockBuilderSplitNode(dock_id_current, ImGuiDir_Down, 1.f-0.618f, NULL, &dock_id_current);
ImGui::DockBuilderSetNodeSize(dock_id_current, ImVec2(avail.x, avail.y*0.618f));
ImGuiID dock_id_left = ImGui::DockBuilderSplitNode(dock_id_current, ImGuiDir_Left, 1.f-0.618f, NULL, &dock_id_current);
ImGui::DockBuilderSetNodeSize(dock_id_current, ImVec2(avail.x*0.618f, avail.y));
ImGuiID dock_id_bets = ImGui::DockBuilderSplitNode(dock_id_current, ImGuiDir_Up, 0.5f, NULL, &dock_id_current);
ImGui::DockBuilderDockWindow(WINDOW_NAME_LOG, dock_id_log);
ImGui::DockBuilderDockWindow(WINDOW_NAME_POINTS, dock_id_left);
ImGui::DockBuilderDockWindow(WINDOW_NAME_CHAT, dock_id_left);
ImGui::DockBuilderDockWindow(WINDOW_NAME_BETS, dock_id_bets);
ImGui::DockBuilderDockWindow(WINDOW_NAME_STATS, dock_id_current);
ImGui::DockBuilderFinish(main_dockspace_id);
}
ImGuiDockNodeFlags flags = ImGuiDockNodeFlags_NoWindowMenuButton | ImGuiDockNodeFlags_NoCloseButton;
if (app.settings.style.auto_hide_tab_bars)
flags |= ImGuiDockNodeFlags_AutoHideTabBar;
ImGui::DockSpace(main_dockspace_id, ImVec2(0.0f, 0.0f), flags);
ImGui::PopStyleVar(); // WindowPadding
}
ImGui::End();
ImGui::PopStyleVar(3);
//////////////////
// DEBUG WINDOW //
//////////////////
#if BETTER_DEBUG
if (app.settings.show_window_debug)
{
if (ImGui::Begin(WINDOW_NAME_DEBUG, &app.settings.show_window_debug))
{
{
static char t[9] = "_.-*^*-.";
char temp = t[0];
memmove(&t[0], &t[1], 7);
t[7] = temp;
ImGui::Text("%s%s%s%s%s%s%s%s", t, t, t, t, t, t, t, t);
}
if (ImGui::BeginTabBar("tabs"))
{
if (ImGui::BeginTabItem("Utilities"))
{
ImGui::Checkbox("Show demo window", &show_demo_window);
ImGui::InputInt("frame_sleep", &frame_sleep);
if (ImGui::InputInt("Spoof file index", &spoof_message_file_index))
{
if (spoof_message_file_index >= 0)
start_reading_spoof_messages(&app);
else
stop_reading_spoof_messages(&app);
}
if (ImGui::InputFloat("Spoof message interval", &spoof_interval))
{
if (spoof_message_file_index >= 0)
start_reading_spoof_messages(&app);
}
ImGui::InputInt("Spoof message chunk", &spoof_chunk_size);
const i32 num_pers = 14200;
if (ImGui::Button("Fill leaderboard"))
{
for (i32 i = 0; i < num_pers; ++i)
{
char name[100];
stbsp_sprintf(name, "person%.5i", i);
UserInfo* user = shget(app.users, name);
if (!user)
{
add_user(&app, name, rand()%10000);
}
}
}
if (ImGui::Button("Fill feedback queue"))
{
for (i32 i = 0; i < num_pers; ++i)
{
char name[20];
stbsp_sprintf(name, "person%.5i", i);
UserInfo* user = shget(app.users, name);
if (user && !app.point_feedback_queue.contains(user))
app.point_feedback_queue.push_back_growing(user);
}
}
if (ImGui::Button("Fill bets") && arrlen(app.bet_options) > 0)
{
for (i32 i = 0; i < num_pers; ++i)
{
char name[100];
stbsp_sprintf(name, "person%.5i", i);
UserInfo* user = shget(app.users, name);
if (user && user->points > 0)
{
register_bet(&app, name, rand()%(user->points), rand()%arrlen(app.bet_options));
}
}
}
if (ImGui::Button("Fill log"))
{
for (i32 i = 0; i < 100; ++i)
{
add_log(&app, i % LOGLEVEL_ENUM_SIZE, "This is a log entry! Log level: %i", i % LOGLEVEL_ENUM_SIZE);
}
}
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("Allocations"))
{
if (ImGui::BeginTable("Allocations", 3, ImGuiTableFlags_Resizable))
{
ImGui::TableSetupColumn("ID");
ImGui::TableSetupColumn("Count");
ImGui::TableSetupColumn("Total size");
ImGui::TableHeadersRow();
for (ost_node* node = ost_inorder_first(app.global_alloc_map);
node != NULL;
node = ost_inorder_next(node))
{
AllocSum* data = (AllocSum*) node->data;
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0);
ImGui::TextUnformatted(data->strid);
ImGui::TableSetColumnIndex(1);
ImGui::Text("%'i", data->count);
ImGui::TableSetColumnIndex(2);
ImGui::Text("%'llu", data->size);
}
ImGui::EndTable();
}
ImGui::EndTabItem();
}
ImGui::EndTabBar();
}
}
ImGui::End();
}
/////////////////
// DEMO WINDOW //
/////////////////
if (show_demo_window)
ImGui::ShowDemoWindow(&show_demo_window);
#endif
/////////////////
// CHAT WINDOW //
/////////////////
if (app.settings.show_window_chat)
{
if (ImGui::Begin(WINDOW_NAME_CHAT, &app.settings.show_window_chat))
{
static bool show_warning = true;
if (show_warning)
{
ImGui::PushStyleColor(ImGuiCol_Text, current_color_set[LOGLEVEL_WARN]);
ImGui::TextWrapped(ICON_FA_EXCLAMATION_TRIANGLE " This view currently does not hide deleted messages. Be careful about capturing it on stream.");
ImGui::PopStyleColor();
if (ImGui::Button("OK")) show_warning = false;
ImGui::Separator();
}
if (app.chat_connected)
{
static bool scroll_to_bottom = false;
static bool is_at_bottom = true;
if (ImGui::BeginChild("ChatScrollingRegion", ImVec2(0,-ImGui::GetFrameHeightWithSpacing())))
{
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0,0));
for (int i = 0; i < app.chat_buffer.count; ++i)
{
if (i % 2)
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.7f, 0.7f, 0.7f, 1.0f));
ImGui::TextWrapped("%s: %s", app.chat_buffer.ref(i)->name, app.chat_buffer.ref(i)->msg);
if (i % 2)
ImGui::PopStyleColor();
}
ImGui::PopStyleVar();
is_at_bottom = ImGui::GetScrollY() >= ImGui::GetScrollMaxY();
if (scroll_to_bottom || is_at_bottom)
ImGui::SetScrollHereY(1.0f); // scroll to bottom of last text item
scroll_to_bottom = false;
}
ImGui::EndChild();
if (!is_at_bottom && ImGui::Button("Scroll to bottom"))
scroll_to_bottom = true;
}
}
ImGui::End();
}
////////////////////////
// LEADERBOARD WINDOW //
////////////////////////
if (app.settings.show_window_points)
{
if (ImGui::Begin(WINDOW_NAME_POINTS, &app.settings.show_window_points))
{
f32 avail_width = ImGui::GetContentRegionAvailWidth() - 2 * ImGui::GetStyle().ItemSpacing.x;
ImGui::SetNextItemWidth(avail_width * 0.5f);
if (ImGui::InputScalar("##handout_amount", ImGuiDataType_U64, &app.settings.handout_amount, &POINTS_STEP_SMALL, &POINTS_STEP_BIG))
{
if (app.settings.handout_amount > POINTS_MAX)
app.settings.handout_amount = POINTS_MAX;
}
ImGui::SameLine();
if (imgui_confirmable_button("Hand out", ImVec2(avail_width * 0.25f, 0), app.color_theme_is_dark, !app.settings.confirm_handout))
{
for (i32 i = 0; i < shlen(app.users); ++i)
{
UserInfo* user = app.users[i].value;
u64 new_amount = BETTER_CLAMP(user->points + app.settings.handout_amount, 0, POINTS_MAX);
// if (new_amount == POINTS_MAX && user->points < new_amount)
// TODO: reinsert user in leaderboard
user->points = new_amount;
}
add_log(&app, LOGLEVEL_INFO, "Handed out %llu %s to all viewers.", app.settings.handout_amount, app.settings.points_name);
}
ImGui::SameLine();
if(imgui_confirmable_button("Reset all", ImVec2(avail_width * 0.25f, 0), app.color_theme_is_dark, !app.settings.confirm_leaderboard_reset))
{
reset_bets(&app);
add_log(&app, LOGLEVEL_INFO, "Resetting everyone's %s to the starting amount.", app.settings.points_name);
for (int i = 0; i < shlen(app.users); ++i)
{
UserInfo* user = app.users[i].value;
user->points = app.settings.starting_points;
}
rebuild_leaderboard(&app);
}
static char user_filter[USERNAME_MAX] = "";
static UserInfo** filtered_users = NULL;
char hint[100];
stbsp_sprintf(hint, "Search %'llu users...", shlen(app.users));
ImGui::SetNextItemWidth(ImGui::GetContentRegionAvailWidth());
if (ImGui::InputTextWithHint("##user_filter", hint, user_filter, USERNAME_MAX, ImGuiInputTextFlags_CharsNoBlank))
{
arrsetlen(filtered_users, 0);
if (user_filter[0] != '\0')
{
make_lower(user_filter);
for (int i = 0; i < shlen(app.users); ++i)
{
if (strstr(app.users[i].key, user_filter))
arrput(filtered_users, app.users[i].value);
}
}
}
if (user_filter[0] != '\0')
{
if (ImGui::BeginTable("##leaderboard_table_default", 3, ImGuiTableFlags_ScrollY | ImGuiTableFlags_RowBg | ImGuiTableFlags_Resizable))
{
ImGui::TableSetupScrollFreeze(0, 1);
ImGui::TableSetupColumn("Rank", ImGuiTableColumnFlags_WidthFixed | ImGuiTableColumnFlags_NoResize);
ImGui::TableSetupColumn("User");
ImGui::TableSetupColumn(points_name_cap);
ImGui::TableHeadersRow();
ImGuiListClipper clipper;
clipper.Begin((int)arrlen(filtered_users));
while (clipper.Step())
{
for (int i = clipper.DisplayStart; i < clipper.DisplayEnd; ++i)
{
UserInfo* user = filtered_users[i];
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0);
u32 rank = get_leaderboard_rank(&app, user->tree_node);
if (rank == 0)
ImGui::TextUnformatted(ICON_FA_TROPHY);
else
ImGui::Text("%u", rank);
ImGui::TableSetColumnIndex(1);
ImGui::TextUnformatted(user->name);
ImGui::TableSetColumnIndex(2);
ImGui::Text("%'llu", user->points);
}
}
ImGui::EndTable();
}
}
else
{
if (ImGui::BeginTable("##leaderboard_table_default", 3, ImGuiTableFlags_ScrollY | ImGuiTableFlags_RowBg | ImGuiTableFlags_Resizable))
{
ImGui::TableSetupScrollFreeze(0, 1);
ImGui::TableSetupColumn("Rank", ImGuiTableColumnFlags_WidthFixed | ImGuiTableColumnFlags_NoResize);
ImGui::TableSetupColumn("User");
ImGui::TableSetupColumn(points_name_cap);
ImGui::TableHeadersRow();
ImGuiListClipper clipper;
auto tree_size = ost_count(&app.leaderboard_tree);
clipper.Begin(tree_size);
while (clipper.Step())
{
ost_node* node = ost_select(&app.leaderboard_tree, (tree_size - 1) - clipper.DisplayStart);
for (int i = clipper.DisplayStart; i < clipper.DisplayEnd; ++i)
{
UserInfo* user = (UserInfo*)node->data;
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0);
u32 rank = get_leaderboard_rank(&app, user->tree_node);
if (rank == 0)
ImGui::TextUnformatted(ICON_FA_TROPHY);
else
ImGui::Text("%u", rank);
ImGui::TableSetColumnIndex(1);
ImGui::TextUnformatted(user->name);
ImGui::TableSetColumnIndex(2);
ImGui::Text("%'llu", user->points);
node = ost_inorder_prev(node);
}
}
ImGui::EndTable();
}
}
}
ImGui::End();
}
/////////////////
// BETS WINDOW //
/////////////////
if (app.settings.show_window_bets)
{
if (ImGui::Begin(WINDOW_NAME_BETS, &app.settings.show_window_bets))
{
ImGui::PushFont(app.font_timer);
char buf[50];
switch (bets_status(&app))
{
case BETS_STATUS_OPEN:
{
i32 seconds_left = (i32)app.timer_left;
stbsp_sprintf(buf, "%i:%.2i", seconds_left/60, seconds_left%60);
} break;
case BETS_STATUS_COYOTE:
{
strcpy(buf, "Bets are closing...");
} break;
case BETS_STATUS_CLOSED:
{
strcpy(buf, "Bets are closed.");
} break;
}
f32 progress = app.settings.timer_setting == 0? 0.f : app.timer_left/app.settings.timer_setting;
f32 hue = BETTER_LERP(0.f, 80.f/255.f, progress);
ImGui::PushStyleColor(ImGuiCol_PlotHistogram, ImColor::HSV(hue, 1.f, .9f).Value);
ImGui::ProgressBar(progress, ImVec2(-1,0), buf);
ImGui::PopStyleColor();
ImGui::PopFont();
bool bets_were_open = bets_status(&app) != BETS_STATUS_CLOSED;
if (bets_were_open) imgui_push_disabled();
if (ImGui::Button("Open bets"))
{
open_bets(&app);
}
ImGui::SameLine();
ImGui::TextUnformatted("for");
ImGui::SameLine();
ImGui::SetNextItemWidth(5 * ImGui::GetFontSize());
ImGui::InputScalar("seconds", ImGuiDataType_S32, &app.settings.timer_setting, &TIMER_STEP_SMALL, &TIMER_STEP_BIG);
app.settings.timer_setting = BETTER_CLAMP(app.settings.timer_setting, 1, TIMER_MAX);
if (bets_were_open) imgui_pop_disabled();
else imgui_push_disabled();
if (ImGui::Button("Close bets")) close_bets(&app);
if (bets_were_open) imgui_push_disabled();
else imgui_pop_disabled();
ImGui::SameLine();
if(imgui_confirmable_button("Refund all bets", ImVec2(0, 0), app.color_theme_is_dark, !app.settings.confirm_refund))
reset_bets(&app);
if (bets_were_open) imgui_pop_disabled();
char* icon_points = (_stricmp(app.settings.points_name, "spoons") == 0)? ICON_FA_SPOON : ICON_FA_MONEY;
ImGui::Text("Totals: %s %'lu %s %'.0f", ICON_FA_HASHTAG, total_number_of_bets, icon_points, grand_total_bets);
imgui_tooltipf("Total number of bets: %'i\nTotal %s: %'.0f", total_number_of_bets, app.settings.points_name, grand_total_bets);
ImGui::Separator();
if (ImGui::BeginChild("Option list"))
{
int removal = -1;
bool bets_exist = grand_total_bets > 0;
for (int i = 0; i < arrlen(app.bet_options); ++i)
{
ImGui::PushID(i);
if (bets_were_open) imgui_push_disabled();
if (bets_exist) imgui_push_disabled();
if (ImGui::Button(ICON_FA_TRASH, ImVec2(ImGui::GetFrameHeight(), 0)))
{
removal = i;
}
if (bets_exist)
{
imgui_pop_disabled();
imgui_tooltip("Refund bets or make a payout before removing options.");
}
ImGui::SameLine();
if(imgui_confirmable_button("Payout", ImVec2(0, 0), app.color_theme_is_dark, !app.settings.confirm_payout))
{
do_payout(&app, i, option_totals[i], grand_total_bets);
}
if (bets_were_open) imgui_pop_disabled();
ImGui::SameLine();
char info_str[50 + POINTS_NAME_MAX];
stbsp_sprintf(info_str, ICON_FA_HASHTAG " %'llu %s %'.0f (%.1f%%)", shlen(app.bet_options[i].bets), icon_points, option_totals[i], (grand_total_bets == 0.0)? 0.0 : 100.0*option_totals[i]/grand_total_bets);
auto info_str_width = ImGui::CalcTextSize(info_str).x;
ImGui::PushFont(app.font_mono);
ImGui::Text("%i", i+1);
ImGui::SameLine();
char option_hint[32];
stbsp_sprintf(option_hint, "Option %i", i+1);
auto avail = ImGui::GetContentRegionAvail().x;
auto name_width = ImGui::CalcTextSize(*app.bet_options[i].option_name? app.bet_options[i].option_name : option_hint).x;
ImGui::SetNextItemWidth(BETTER_MAX(name_width + ImGui::GetStyle().FramePadding.x*2, avail - info_str_width - ImGui::GetStyle().FramePadding.x*2));
if (ImGui::InputTextWithHint("", option_hint, app.bet_options[i].option_name, sizeof(app.bet_options[i].option_name)))
{
while(*app.bet_options[i].option_name && (*app.bet_options[i].option_name == ' ' || is_digit(*app.bet_options[i].option_name)))
{
trim_whitespace(app.bet_options[i].option_name);
// Trim leading digits
char* p = app.bet_options[i].option_name;
while(is_digit(*p)) ++p;
memmove(app.bet_options[i].option_name, p, strlen(p)+1);
}
collapse_spaces(app.bet_options[i].option_name);
// Make sure the name isn't taken!
for (int other_i = 0; other_i < arrlen(app.bet_options); ++other_i)
{
if (other_i == i) continue;
if (_stricmp(app.bet_options[i].option_name, app.bet_options[other_i].option_name) == 0)
{
app.bet_options[i].option_name[0] = '\0';
}
}
}
ImGui::SameLine();
ImGui::PopFont();
ImGui::TextUnformatted(info_str);
imgui_tooltipf("%s\nNumber of bets: %'lu\n%s: %'.0f",
*app.bet_options[i].option_name? app.bet_options[i].option_name : option_hint,
shlen(app.bet_options[i].bets),
points_name_cap,
option_totals[i]);
ImGui::Separator();
ImGui::PopID();
}
if (removal != -1)
{
BetTable_destroy(&app.bet_options[removal]);
arrdel(app.bet_options, removal);
arrdel(app.bet_history, removal);
}
if (bets_were_open || bets_exist) imgui_push_disabled();
if (ImGui::Button(ICON_FA_PLUS, ImVec2(ImGui::GetFrameHeight(), 0)))
{
BetTable bt = {};
BetTable_init(&bt);
arrput(app.bet_options, bt);
arraddnptr(app.bet_history, 1);
arrput(option_totals, 0);
}
if (bets_were_open || bets_exist) imgui_pop_disabled();
if (bets_exist) imgui_tooltip("Refund bets or make a payout before adding options.");
}
ImGui::EndChild();
}
ImGui::End();
}
//////////////////
// STATS WINDOW //
//////////////////
if (app.settings.show_window_statistics)
{
char** labels = (char**) BETTER_ALLOC(sizeof(char*) * arrlen(app.bet_options), "stats_window_labels");
f64* positions = (f64*) BETTER_ALLOC(sizeof(f64) * arrlen(app.bet_options), "stats_window_positions");
for (int i = 0; i < arrlen(app.bet_options); ++i)
labels[i] = (char*) BETTER_ALLOC(OPTION_NAME_MAX + 1, "stats_window_labels_inner");
ImGui::SetNextWindowSize(ImVec2(300, 200), ImGuiCond_FirstUseEver);
ImGui::SetNextWindowPos(ImVec2(display_w * 0.5f - 150.f, display_h * 0.5f - 100.f), ImGuiCond_FirstUseEver);
if (ImGui::Begin(WINDOW_NAME_STATS, &app.settings.show_window_statistics))
{
if (app.shark)
{
ImGui::Text("Top shark: %s (won %llu)", app.shark->name, app.shark_win);
}
if (app.fish)
{
if (app.shark)
ImGui::SameLine();
ImGui::Text("Biggest fish: %s (lost %llu)", app.fish->name, app.fish_loss);
}
for (int i = 0; i < arrlen(app.bet_options); ++i)
{
if (app.bet_options[i].option_name[0] != '\0')
stbsp_sprintf(labels[i], "%s", app.bet_options[i].option_name);
else
stbsp_sprintf(labels[i], "Option %i", i+1);
positions[i] = i;
}
char y_name[POINTS_NAME_MAX + 10];
stbsp_sprintf(y_name, "%s bet", points_name_cap);
enum
{
VIEW_CURRENT,
VIEW_TIMELINE,
};
enum
{
PLOT_PIE,
PLOT_BARS,
};
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(0,0));
static int view = VIEW_CURRENT, plot = PLOT_PIE;
if (ImGui::RadioButton("Current", view == VIEW_CURRENT)) view = VIEW_CURRENT;
ImGui::SameLine();
if (ImGui::RadioButton("Timeline", view == VIEW_TIMELINE)) view = VIEW_TIMELINE;
if (view == VIEW_CURRENT)
{
ImGui::SameLine(0, 50.0f);
if (ImGui::RadioButton("Pie", plot == PLOT_PIE)) plot = PLOT_PIE;
ImGui::SameLine();
if (ImGui::RadioButton("Bars", plot == PLOT_BARS)) plot = PLOT_BARS;
}
ImGui::PopStyleVar();
switch (view)
{
case VIEW_CURRENT:
{
switch (plot)
{
case PLOT_PIE:
{
static ImVec2 plot_size(1,1);
ImPlot::SetNextPlotLimits(0, plot_size.x, 0, plot_size.y, ImGuiCond_Always);
if (ImPlot::BeginPlot("##pie", NULL, NULL, ImVec2(-1, -1),
ImPlotFlags_NoMenus | ImPlotFlags_NoBoxSelect | ImPlotFlags_NoMousePos,
ImPlotAxisFlags_NoDecorations,
ImPlotAxisFlags_NoDecorations))
{
plot_size = ImPlot::GetPlotSize();
ImPlot::PlotPieChart(labels, option_totals, (i32)arrlen(app.bet_options), plot_size.x*0.5, plot_size.y*0.5, BETTER_MIN(plot_size.x, plot_size.y)*0.5-5.0, true, "%.0f (%.1f%%)", 90, true);
ImPlot::EndPlot();
}
} break;
case PLOT_BARS:
{
ImPlot::SetNextPlotTicksX(positions, (i32)arrlen(app.bet_options), labels);
ImPlot::SetNextPlotLimitsX(-0.5, arrlen(app.bet_options)-0.5, ImGuiCond_Always);
ImPlot::FitNextPlotAxes();
if (ImPlot::BeginPlot("##bars", NULL, y_name, ImVec2(-1,-1),
ImPlotFlags_NoMenus | ImPlotFlags_NoBoxSelect | ImPlotFlags_NoMousePos,
ImPlotAxisFlags_None,
ImPlotAxisFlags_LockMin))
{
ImPlot::PlotBarsG("", bar_chart_getter, &app, (i32)arrlen(app.bet_options), 0.9);
if (grand_total_bets > 0)
for (i32 i = 0; i < arrlen(app.bet_options); ++i)
{
char bar_text[100];
stbsp_sprintf(bar_text, "%'.0f (%.1f%%)", option_totals[i], 100.0*option_totals[i]/grand_total_bets);
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0,0,0,1));
ImPlot::PlotText(bar_text, i, 0, false, ImVec2(0,-10));
ImGui::PopStyleColor();
}
ImPlot::EndPlot();
}
} break;
default: assert(false);
}
} break;
case VIEW_TIMELINE:
{
ImPlot::SetNextPlotLimitsX(0, (f32)(app.settings.timer_setting+app.settings.coyote_time), ImGuiCond_Always);
ImPlot::FitNextPlotAxes();
if (ImPlot::BeginPlot("##timeline", NULL, y_name, ImVec2(-1,-1),
ImPlotFlags_NoMenus | ImPlotFlags_NoBoxSelect | ImPlotFlags_NoMousePos,
ImPlotAxisFlags_None,
ImPlotAxisFlags_LockMin))
{
for (int i = 0; i < arrlen(app.bet_history); ++i)
{
ImPlot::PlotLine(labels[i], app.bet_history[i], timeline_current_index+1, (f32)(app.settings.timer_setting+app.settings.coyote_time)/(f32)TIMELINE_SAMPLE_COUNT);
}
ImPlot::EndPlot();
}
} break;
default: assert(false);
}
}
ImGui::End();
for (int i = 0; i < arrlen(app.bet_options); ++i)
BETTER_FREE(labels[i]);
BETTER_FREE(labels);
BETTER_FREE(positions);
}
////////////////
// LOG WINDOW //
////////////////
if (app.settings.show_window_log)
{
if (app.should_focus_log_window)
{
app.should_focus_log_window = false;
ImGui::SetNextWindowFocus();
ImGui::SetNextWindowCollapsed(false);
}
if (ImGui::Begin(WINDOW_NAME_LOG, &app.settings.show_window_log))
{
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(0,0));
ImGui::TextUnformatted("Filter"); ImGui::SameLine();
#if BETTER_DEBUG
ImGui::Checkbox("Debug", &app.log_filter[LOGLEVEL_DEBUG]); ImGui::SameLine();
#endif
ImGui::Checkbox("Info", &app.log_filter[LOGLEVEL_INFO]); ImGui::SameLine();
ImGui::Checkbox("Warnings", &app.log_filter[LOGLEVEL_WARN]); ImGui::SameLine();
if (ImGui::Checkbox("Errors", &app.log_filter[LOGLEVEL_USERERROR]))
app.log_filter[LOGLEVEL_DEVERROR] = app.log_filter[LOGLEVEL_USERERROR];
if (!app.log_filter[LOGLEVEL_USERERROR] && app.unread_error != -1)
{
ImGui::SameLine();
ImGui::TextColored(current_color_set[LOGLEVEL_USERERROR], "*");
}
ImGui::PopStyleVar();
ImGui::Separator();
static bool goto_error = false;
static bool scroll_to_bottom = false;
static bool is_at_bottom = true;
if (ImGui::BeginChild("LogScrollingRegion", ImVec2(0,-ImGui::GetFrameHeightWithSpacing())))
{
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0,0));
for (int i = 0; i < app.log_buffer.count; ++i)
{
if (!app.log_filter[app.log_buffer.ref(i)->level]) continue;
ImGui::PushStyleColor(ImGuiCol_Text, current_color_set[app.log_buffer.ref(i)->level]);
ImGui::TextWrapped(app.log_buffer.ref(i)->content);
if (i == app.unread_error)
{
if (ImGui::IsItemVisible())
app.unread_error = -1;
else if (goto_error)
{
ImGui::SetScrollHereY(0.5f);
scroll_to_bottom = false;
goto_error = false;
}
}
ImGui::PopStyleColor();
}
ImGui::PopStyleVar();
is_at_bottom = ImGui::GetScrollY() >= ImGui::GetScrollMaxY();
if (scroll_to_bottom || is_at_bottom)
ImGui::SetScrollHereY(1.0f); // scroll to bottom of last text item
scroll_to_bottom = false;
}
ImGui::EndChild();
if (!is_at_bottom)
{
if (ImGui::Button("Scroll to bottom"))
scroll_to_bottom = true;
}
if (app.unread_error != -1)
{
if (!is_at_bottom)
ImGui::SameLine();
if (ImGui::Button("Go to last error"))
{
app.log_filter[LOGLEVEL_USERERROR] = true;
goto_error = true;
}
}
}
ImGui::End();
}
/////////////////////
// SETTINGS WINDOW //
/////////////////////
if (app.settings.show_window_settings)
{
ImGui::SetNextWindowSize(ImVec2(600.f, 400.f), ImGuiCond_FirstUseEver);
ImGui::SetNextWindowPos(ImVec2(display_w * 0.5f - 300.f, display_h * 0.5f - 200.f), ImGuiCond_FirstUseEver);
if (ImGui::Begin(WINDOW_NAME_SETTINGS, &app.settings.show_window_settings))
{
ImGui::SetNextItemOpen(true, ImGuiCond_FirstUseEver);
bool header_twitch = ImGui::CollapsingHeader(ICON_FA_TWITCH " Twitch");
imgui_tooltip("Connection, login info...");
if (header_twitch)
{
bool dns_was_running = dns_thread_running(&app);
if (dns_was_running) imgui_push_disabled();
bool irc_connected = app.sock != INVALID_SOCKET || dns_was_running;
if (irc_connected) imgui_push_disabled();
// TODO: "reconnect"
if (ImGui::Button("Connect"))
{
if (!irc_connect(&app))
{
// TODO: Errors should already be handled in irc_connect, but maybe provide feedback
}
}
if (irc_connected) imgui_pop_disabled();
ImGui::SameLine();
if (ImGui::Button("Disconnect"))
{
irc_disconnect(&app);
}
if (dns_was_running) imgui_pop_disabled();
if (ImGui::BeginTable("settings_twitch", 2))
{
ImGui::TableSetupColumn(NULL, ImGuiTableColumnFlags_WidthFixed | ImGuiTableColumnFlags_NoResize);
ImGui::TableNextColumn();
ImGui::TextUnformatted("Channel");
ImGui::TableNextColumn();
f32 widget_width = ImGui::GetContentRegionAvailWidth() - 2.0f * ImGui::GetStyle().FramePadding.x;
ImGui::SetNextItemWidth(widget_width);
if (irc_connected) imgui_push_disabled();
if (ImGui::InputText("##channel", app.settings.channel, CHANNEL_NAME_MAX, ImGuiInputTextFlags_CharsNoBlank))
make_lower(app.settings.channel);
if (irc_connected) imgui_pop_disabled();
ImGui::TableNextColumn();
ImGui::TextUnformatted("Auto-connect");
imgui_extra("If enabled, Better auto-connects to the channel on startup.");
ImGui::TableNextColumn();
ImGui::Checkbox("##autoconnect", &app.settings.auto_connect);
ImGui::TableNextColumn();
ImGui::TextUnformatted("Username");
imgui_extra("The username of the account the bot will log in as.\n");
ImGui::TableNextColumn();
ImGui::SetNextItemWidth(widget_width);
if (irc_connected) imgui_push_disabled();
if (ImGui::InputText("##username", app.settings.username, CHANNEL_NAME_MAX, ImGuiInputTextFlags_CharsNoBlank))
make_lower(app.settings.username);
if (irc_connected) imgui_pop_disabled();
ImGui::TableNextColumn();
ImGui::TextUnformatted("OAuth token");
imgui_extra("Go to twitchapps.com/tmi to get a token for your account. Must start with \"oauth:\". The clipboard will be emptied after pasting.");
ImGui::TableNextColumn();
ImGui::SetNextItemWidth(widget_width);
if (irc_connected) imgui_push_disabled();
if (!app.settings.oauth_token_is_present)
{
ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted("(empty)");
ImGui::SameLine();
bool clip_open = OpenClipboard(app.main_wnd);
bool disable_paste = !IsClipboardFormatAvailable(CF_TEXT) ||
!clip_open;
if (disable_paste) imgui_push_disabled();
if (ImGui::Button("Paste##paste_oauth_token"))
{
HANDLE clip_handle = GetClipboardData(CF_TEXT);
if (clip_handle == NULL)
{
add_log(&app, LOGLEVEL_DEVERROR, "GetClipboardData failed: %d", GetLastError());
}
else
{
char* clip_data = (char*) GlobalLock(clip_handle);
if (clip_data == NULL)
{
add_log(&app, LOGLEVEL_DEVERROR, "GlobalLock failed: %d", GetLastError());
}
else
{
strncpy(app.settings.token, clip_data, TOKEN_MAX);
GlobalUnlock(clip_handle);
if(strncmp(app.settings.token, "oauth:", 6) != 0)
{
add_log(&app, LOGLEVEL_USERERROR, "Pasted token has an incorrect format. Make sure it starts with \"oauth:\".");
SecureZeroMemory(app.settings.token, sizeof(app.settings.token));
app.settings.oauth_token_is_present = false;
}
else
{
if (!EmptyClipboard()) add_log(&app, LOGLEVEL_DEVERROR, "EmptyClipboard failed: %d", GetLastError());
// Trim trailing whitespace in token
char* c = app.settings.token;
while (*c && *c != ' ' && *c != '\r' && *c != '\n') ++c;
*c = '\0';
if (!CryptProtectMemory(app.settings.token, sizeof(app.settings.token), CRYPTPROTECTMEMORY_SAME_PROCESS))
{
add_log(&app, LOGLEVEL_DEVERROR, "CryptProtectMemory failed: %i", GetLastError());
SecureZeroMemory(app.settings.token, sizeof(app.settings.token));
app.settings.oauth_token_is_present = false;
}
else app.settings.oauth_token_is_present = true;
}
}
}
}
if (disable_paste) imgui_pop_disabled();
if (clip_open) CloseClipboard();
}
else
{
ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted("(hidden)");
ImGui::SameLine();
if (imgui_confirmable_button("Clear##clear_oauth_token", ImVec2(0, 0), app.color_theme_is_dark))
{
SecureZeroMemory(app.settings.token, sizeof(app.settings.token));
app.settings.oauth_token_is_present = false;
}
}
if (irc_connected) imgui_pop_disabled();
ImGui::TableNextColumn();
ImGui::TextUnformatted("User is moderator");
imgui_extra("This allows Better to send messages to the chat more frequently to keep up with large viewer groups. Only enable this if the user is a moderator on (or the owner of) the channel, or you might be temporarily blocked by Twitch.");
ImGui::TableNextColumn();
ImGui::Checkbox("##mod_mode", &app.settings.is_mod);
ImGui::EndTable();
}
}
bool header_betting = ImGui::CollapsingHeader(ICON_FA_BOOK " Betting");
imgui_tooltip("Betting behavior, chat commands...");
if (header_betting)
{
if (ImGui::BeginTable("settings_betting", 2))
{
ImGui::TableSetupColumn(NULL, ImGuiTableColumnFlags_WidthFixed | ImGuiTableColumnFlags_NoResize);
ImGui::TableNextColumn();
ImGui::TextUnformatted("Command prefix");
ImGui::TableNextColumn();
f32 widget_width = ImGui::GetContentRegionAvailWidth() - 2.0f * ImGui::GetStyle().FramePadding.x;
ImGui::SetNextItemWidth(widget_width);
ImGui::InputText("##command_prefix", app.settings.command_prefix, 2, ImGuiInputTextFlags_CharsNoBlank);
ImGui::TableNextColumn();
ImGui::TextUnformatted("Currency name");
ImGui::TableNextColumn();
ImGui::SetNextItemWidth(widget_width);
if (ImGui::InputText("##points_name", app.settings.points_name, POINTS_NAME_MAX, ImGuiInputTextFlags_CharsNoBlank))
make_lower(app.settings.points_name);
ImGui::Text("Command: %s%s", app.settings.command_prefix, app.settings.points_name);
ImGui::TableNextColumn();
bool bets_open = bets_status(&app) != BETS_STATUS_CLOSED;
ImGui::Text("Starting %s", app.settings.points_name);
ImGui::TableNextColumn();
ImGui::SetNextItemWidth(widget_width);
ImGui::InputScalar("##starting_points", ImGuiDataType_U64, &app.settings.starting_points, &POINTS_STEP_SMALL, &POINTS_STEP_BIG/*, NULL, ImGuiInputTextFlags_EnterReturnsTrue*/);
ImGui::TableNextColumn();
ImGui::TextUnformatted("Allow multibets");
imgui_extra("If enabled, viewers can place bets on multiple options at the same time. If disabled, placing a bet on one option will remove the viewer's bets on all other options.");
ImGui::TableNextColumn();
if (bets_open) imgui_push_disabled();
ImGui::Checkbox("##allow_multibets", &app.settings.allow_multibets);
if (bets_open) imgui_pop_disabled();
ImGui::TableNextColumn();
ImGui::TextUnformatted("Bet update mode");
imgui_extra("This option controls what happens if a viewer places a bet on an option where they already have a wager.\n\nSet mode: The existing wager is replaced by the input amount.\nAdd mode: The input amount is added to the existing wager.");
ImGui::TableNextColumn();
if (bets_open) imgui_push_disabled();
if (ImGui::RadioButton("Set##update_mode_set", !app.settings.add_mode))
app.settings.add_mode = false;
ImGui::SameLine();
if (ImGui::RadioButton("Add##update_mode_add", app.settings.add_mode))
app.settings.add_mode = true;
if (bets_open) imgui_pop_disabled();
ImGui::TableNextColumn();
ImGui::TextUnformatted("Timer leniency");
imgui_extra("Bets will be open for this amount of seconds after the timer apparently runs out.");
ImGui::TableNextColumn();
ImGui::SetNextItemWidth(widget_width);
if (bets_open) imgui_push_disabled();
ImGui::InputScalar("##timer_leniency", ImGuiDataType_U32, &app.settings.coyote_time, &TIMER_STEP_SMALL, &TIMER_STEP_BIG);
if (bets_open) imgui_pop_disabled();
ImGui::EndTable();
}
}
bool header_announcements = ImGui::CollapsingHeader(ICON_FA_BULLHORN " Announcements");
imgui_tooltip("Choose what announcements Better will make in the chat.");
if (header_announcements)
{
if (ImGui::BeginTable("settings_announcements", 2))
{
ImGui::TableSetupColumn(NULL, ImGuiTableColumnFlags_WidthFixed | ImGuiTableColumnFlags_NoResize);
ImGui::TableNextColumn();
ImGui::TextUnformatted("Bets open");
ImGui::TableNextColumn();
ImGui::Checkbox("##announce_bets_open", &app.settings.announce_bets_open);
ImGui::TableNextColumn();
ImGui::TextUnformatted("Bets close");
ImGui::TableNextColumn();
ImGui::Checkbox("##announce_bets_close", &app.settings.announce_bets_close);
ImGui::TableNextColumn();
ImGui::TextUnformatted("Payout");
ImGui::TableNextColumn();
ImGui::Checkbox("##announce_payout", &app.settings.announce_payout);
ImGui::EndTable();
}
}
bool header_confirmation = ImGui::CollapsingHeader(ICON_FA_EXCLAMATION_TRIANGLE " Confirmation");
imgui_tooltip("Enable click-twice confirmation for functions that you don't want to click accidentally.");
if (header_confirmation)
{
if (ImGui::BeginTable("settings_confirmation", 2))
{
ImGui::TableSetupColumn(NULL, ImGuiTableColumnFlags_WidthFixed | ImGuiTableColumnFlags_NoResize);
ImGui::TableNextColumn();
ImGui::TextUnformatted("Handouts");
ImGui::TableNextColumn();
ImGui::Checkbox("##confirm_handout", &app.settings.confirm_handout);
ImGui::TableNextColumn();
ImGui::TextUnformatted("Resetting leaderboard");
ImGui::TableNextColumn();
ImGui::Checkbox("##confirm_leaderboard_reset", &app.settings.confirm_leaderboard_reset);
ImGui::TableNextColumn();
ImGui::TextUnformatted("Refunding bets");
ImGui::TableNextColumn();
ImGui::Checkbox("##confirm_refund", &app.settings.confirm_refund);
ImGui::TableNextColumn();
ImGui::TextUnformatted("Payouts");
ImGui::TableNextColumn();
ImGui::Checkbox("##confirm_payout", &app.settings.confirm_payout);
ImGui::TableNextColumn();
ImGui::EndTable();
}
}
bool header_style = ImGui::CollapsingHeader(ICON_FA_PAINT_BRUSH " Style");
imgui_tooltip("Edit UI appearance.");
if (header_style)
{
if (imgui_confirmable_button("Restore default style", ImVec2(0,0), app.color_theme_is_dark, false))
{
app.settings.style = Settings::Style();
update_imgui_style(&app);
should_rebuild_fonts = true;
}
ImGui::TableNextColumn();
if (ImGui::BeginTable("settings_style", 2))
{
ImGui::TableSetupColumn(NULL, ImGuiTableColumnFlags_WidthFixed | ImGuiTableColumnFlags_NoResize);
ImGui::TableNextColumn();
ImGui::TextUnformatted("Font size (normal)");
ImGui::TableNextColumn();
f32 widget_width = ImGui::GetContentRegionAvailWidth() - 2.0f * ImGui::GetStyle().FramePadding.x;
ImGui::SetNextItemWidth(widget_width);
if (ImGui::DragInt("##font_size_normal", &app.settings.style.font_size_normal, 0.2f, FONT_SIZE_MIN, FONT_SIZE_MAX, "%d", ImGuiSliderFlags_AlwaysClamp))
should_rebuild_fonts = true;
if (!ImGui::IsItemActive()) imgui_tooltip("Drag or double click");
ImGui::TableNextColumn();
ImGui::TextUnformatted("Font size (timer)");
ImGui::TableNextColumn();
ImGui::SetNextItemWidth(widget_width);
if (ImGui::DragInt("##font_size_timer", &app.settings.style.font_size_timer, 0.2f, FONT_SIZE_MIN, FONT_SIZE_MAX, "%d", ImGuiSliderFlags_AlwaysClamp))
should_rebuild_fonts = true;
if (!ImGui::IsItemActive()) imgui_tooltip("Drag or double click");
ImGui::TableNextColumn();
ImGui::TextUnformatted("Color theme");
ImGui::TableNextColumn();
if (ImGui::RadioButton("Auto", app.settings.style.color_theme == COLOR_THEME_AUTO))
{
app.settings.style.color_theme = COLOR_THEME_AUTO;
update_imgui_style(&app);
}
if (ImGui::IsItemHovered()) imgui_tooltip("Follow the system theme.");
ImGui::SameLine();
if (ImGui::RadioButton("Dark", app.settings.style.color_theme == COLOR_THEME_DARK))
{
app.settings.style.color_theme = COLOR_THEME_DARK;
update_imgui_style(&app);
}
ImGui::SameLine();
if (ImGui::RadioButton("Light", app.settings.style.color_theme == COLOR_THEME_LIGHT))
{
app.settings.style.color_theme = COLOR_THEME_LIGHT;
update_imgui_style(&app);
}
ImGui::TableNextColumn();
ImGui::TextUnformatted("Auto hide tab bars");
imgui_extra("Save some screen space by minimizing tab bars that have only one tab. Change only applies to windows after you re-dock them.");
ImGui::TableNextColumn();
ImGui::Checkbox("##auto_hide_tab_bars", &app.settings.style.auto_hide_tab_bars);
ImGui::TableNextColumn();
ImGui::EndTable();
}
}
}
ImGui::End();
}
ImGui::Render();
g_pd3dDeviceContext->OMSetRenderTargets(1, &g_mainRenderTargetView, NULL);
g_pd3dDeviceContext->ClearRenderTargetView(g_mainRenderTargetView, (float*)&clear_color);
ImGui_ImplDX11_RenderDrawData(ImGui::GetDrawData());
// Update and Render additional Platform Windows
if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable)
{
ImGui::UpdatePlatformWindows();
ImGui::RenderPlatformWindowsDefault();
}
g_pSwapChain->Present(1, 0); // Present with vsync
// g_pSwapChain->Present(0, 0); // Present without vsync
}
copy_imgui_ini(&app);
// Cleanup
ImGui_ImplDX11_Shutdown();
ImGui_ImplWin32_Shutdown();
ImGui::DestroyContext();
ImPlot::DestroyContext();
CleanupDeviceD3D();
::DestroyWindow(app.main_wnd);
::UnregisterClass(wc.lpszClassName, wc.hInstance);
irc_disconnect(&app);
irc_cleanup(&app);
save_leaderboard_to_disk(&app);
save_settings_to_disk(&app);
SecureZeroMemory(app.settings.token, sizeof(app.settings.token));
add_log(&app, LOGLEVEL_DEBUG, "Program shutting down.");
BETTER_FREE(app.base_dir);
app.write_queue.destroy();
app.privmsg_queue.destroy();
app.read_queue.destroy();
for (int i = 0; i < shlen(app.users); ++i)
BETTER_FREE(app.users[i].value);
shfree(app.users);
ost_remove_all(&app.leaderboard_tree);
for (int i = 0; i < arrlen(app.bet_options); ++i)
BetTable_destroy(&app.bet_options[i]);
arrfree(app.bet_options);
arrfree(app.bet_history);
app.log_buffer.destroy();
app.chat_buffer.destroy();
BETTER_FREE(app.settings.imgui_ini);
return 0;
}
// Helper functions
bool CreateDeviceD3D(HWND hWnd)
{
// Setup swap chain
DXGI_SWAP_CHAIN_DESC sd;
ZeroMemory(&sd, sizeof(sd));
sd.BufferCount = 2;
sd.BufferDesc.Width = 0;
sd.BufferDesc.Height = 0;
sd.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
sd.BufferDesc.RefreshRate.Numerator = 60;
sd.BufferDesc.RefreshRate.Denominator = 1;
sd.Flags = DXGI_SWAP_CHAIN_FLAG_ALLOW_MODE_SWITCH;
sd.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
sd.OutputWindow = hWnd;
sd.SampleDesc.Count = 1;
sd.SampleDesc.Quality = 0;
sd.Windowed = TRUE;
sd.SwapEffect = DXGI_SWAP_EFFECT_DISCARD;
UINT createDeviceFlags = 0;
//createDeviceFlags |= D3D11_CREATE_DEVICE_DEBUG;
D3D_FEATURE_LEVEL featureLevel;
const D3D_FEATURE_LEVEL featureLevelArray[6] =
{
D3D_FEATURE_LEVEL_11_0,
D3D_FEATURE_LEVEL_10_1,
D3D_FEATURE_LEVEL_10_0,
D3D_FEATURE_LEVEL_9_3,
D3D_FEATURE_LEVEL_9_2,
D3D_FEATURE_LEVEL_9_1,
};
auto hr = D3D11CreateDeviceAndSwapChain(NULL, D3D_DRIVER_TYPE_HARDWARE, NULL, createDeviceFlags, featureLevelArray, 2, D3D11_SDK_VERSION, &sd, &g_pSwapChain, &g_pd3dDevice, &featureLevel, &g_pd3dDeviceContext);
if (hr != S_OK)
{
fprintf(stderr, "Failed to create hardware D3D device (0x%.8X), falling back to software renderer (WARP).", hr);
hr = D3D11CreateDeviceAndSwapChain(NULL, D3D_DRIVER_TYPE_WARP, NULL, createDeviceFlags, featureLevelArray, 2, D3D11_SDK_VERSION, &sd, &g_pSwapChain, &g_pd3dDevice, &featureLevel, &g_pd3dDeviceContext);
if (hr != S_OK)
{
fprintf(stderr, "Failed to create software D3D device (0x%.8X).", hr);
return false;
}
}
CreateRenderTarget();
return true;
}
void CleanupDeviceD3D()
{
CleanupRenderTarget();
if (g_pSwapChain) { g_pSwapChain->Release(); g_pSwapChain = NULL; }
if (g_pd3dDeviceContext) { g_pd3dDeviceContext->Release(); g_pd3dDeviceContext = NULL; }
if (g_pd3dDevice) { g_pd3dDevice->Release(); g_pd3dDevice = NULL; }
}
void CreateRenderTarget()
{
ID3D11Texture2D* pBackBuffer;
g_pSwapChain->GetBuffer(0, IID_PPV_ARGS(&pBackBuffer));
g_pd3dDevice->CreateRenderTargetView(pBackBuffer, NULL, &g_mainRenderTargetView);
pBackBuffer->Release();
}
void CleanupRenderTarget()
{
if (g_mainRenderTargetView) { g_mainRenderTargetView->Release(); g_mainRenderTargetView = NULL; }
}
#ifndef WM_DPICHANGED
#define WM_DPICHANGED 0x02E0 // From Windows SDK 8.1+ headers
#endif
// Forward declare message handler from imgui_impl_win32.cpp
extern IMGUI_IMPL_API LRESULT ImGui_ImplWin32_WndProcHandler(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam);
// Win32 message handler
LRESULT WINAPI WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
if (ImGui_ImplWin32_WndProcHandler(hWnd, msg, wParam, lParam))
return true;
switch (msg)
{
case WM_SIZE:
if (g_pd3dDevice != NULL && wParam != SIZE_MINIMIZED)
{
CleanupRenderTarget();
g_pSwapChain->ResizeBuffers(0, (UINT)LOWORD(lParam), (UINT)HIWORD(lParam), DXGI_FORMAT_UNKNOWN, 0);
CreateRenderTarget();
}
return 0;
case WM_SYSCOMMAND:
if ((wParam & 0xfff0) == SC_KEYMENU) // Disable ALT application menu
return 0;
break;
case WM_DESTROY:
::PostQuitMessage(0);
return 0;
case WM_DPICHANGED:
if (ImGui::GetIO().ConfigFlags & ImGuiConfigFlags_DpiEnableScaleViewports)
{
//const int dpi = HIWORD(wParam);
//printf("WM_DPICHANGED to %d (%.0f%%)\n", dpi, (float)dpi / 96.0f * 100.0f);
const RECT* suggested_rect = (RECT*)lParam;
::SetWindowPos(hWnd, NULL, suggested_rect->left, suggested_rect->top, suggested_rect->right - suggested_rect->left, suggested_rect->bottom - suggested_rect->top, SWP_NOZORDER | SWP_NOACTIVATE);
}
break;
case WM_GETMINMAXINFO:
{
MINMAXINFO* mminfo = (MINMAXINFO*) lParam;
mminfo->ptMinTrackSize.x = WINDOW_MIN_X;
mminfo->ptMinTrackSize.y = WINDOW_MIN_Y;
return 0;
}
case WM_TIMER:
{
switch ((UINT_PTR)wParam)
{
case TID_SPOOF_MESSAGES: {
#if BETTER_DEBUG
read_spoof_messages(&app);
#endif
return 0;
}
case TID_ALLOW_AUTO_RECONNECT: {
app.allow_auto_reconnect = true;
if (!KillTimer(app.main_wnd, TID_ALLOW_AUTO_RECONNECT))
add_log(&app, LOGLEVEL_DEVERROR, "KillTimer failed: %d", GetLastError());
return 0;
}
case TID_PRIVMSG_READY: {
app.privmsg_ready = true;
if (!KillTimer(app.main_wnd, TID_PRIVMSG_READY))
add_log(&app, LOGLEVEL_DEVERROR, "KillTimer failed: %d", GetLastError());
return 0;
}
}
} break;
case WM_SETTINGCHANGE:
if (app.settings.style.color_theme == COLOR_THEME_AUTO &&
lParam &&
lstrcmp(LPCTSTR(lParam), L"ImmersiveColorSet") == 0)
{
update_imgui_style(&app);
return 0;
}
else break;
case BETTER_WM_DNS_COMPLETE:
irc_on_dns_complete((App*)wParam, (addrinfo*)lParam);
return 0;
case BETTER_WM_DNS_FAILED:
irc_on_dns_failed((App*)wParam, (DWORD)lParam);
return 0;
case BETTER_WM_SOCK_MSG:
SOCKET sock = (SOCKET)wParam;
if (sock != app.sock) return 0;
i32 err = WSAGETSELECTERROR(lParam);
switch (WSAGETSELECTEVENT(lParam))
{
case FD_CONNECT:
if (err != 0)
add_log(&app, LOGLEVEL_DEVERROR, "Failed to connect socket: %i\n", err);
else
irc_on_connect(&app);
break;
case FD_WRITE:
irc_on_write(&app);
break;
case FD_READ:
case FD_CLOSE:
irc_on_read_or_close(&app);
break;
}
return 0;
}
return ::DefWindowProc(hWnd, msg, wParam, lParam);
}