tt

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs

commit 5b6e9451f274bcf535fde2c7624e8d723112cced
Author: Pablo Cárdenas <pablo.cardenas@imca.edu.pe>
Date:   Fri, 21 Jun 2024 23:46:40 -0500

Initial Commit

Diffstat:
A.clang-format | 12++++++++++++
A.gitignore | 5+++++
AMakefile | 13+++++++++++++
Afetch.sh | 4++++
Att.c | 360+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Att.h | 7+++++++
Attserver.c | 125+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
7 files changed, 526 insertions(+), 0 deletions(-)

diff --git a/.clang-format b/.clang-format @@ -0,0 +1,12 @@ +BasedOnStyle: LLVM +IndentWidth: 8 +UseTab: Always +BreakBeforeBraces: Linux +AllowShortIfStatementsOnASingleLine: false +IndentCaseLabels: false + +ContinuationIndentWidth: 8 +BinPackArguments: false +BinPackParameters: false +AlignAfterOpenBracket: AlwaysBreak +AllowAllParametersOfDeclarationOnNextLine: false diff --git a/.gitignore b/.gitignore @@ -0,0 +1,5 @@ +/tt.o +/tt +/ttserver.o +/ttserver +/data/ diff --git a/Makefile b/Makefile @@ -0,0 +1,13 @@ +CFLAGS = -g +LDFLAGS = `pkg-config --libs ncurses` `pkg-config --libs json-c` + +all: tt ttserver + +ttcli.o: tt.h + +ttsrv.o: tt.h + +clean: + rm -f tt.o tt ttserver.o ttserver + +.PHONY: all clean diff --git a/fetch.sh b/fetch.sh @@ -0,0 +1,4 @@ +#!/bin/sh + +mkdir -p data +curl https://monkeytype.com/quotes/english.json -o data/quotes_english.json diff --git a/tt.c b/tt.c @@ -0,0 +1,360 @@ +#include "tt.h" +#include <arpa/inet.h> +#include <ctype.h> +#include <ncurses.h> +#include <netinet/in.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/socket.h> +#include <time.h> +#include <unistd.h> + +#define AHEAD_SHOW 4 +#define AHEAD_HIDE 2 +#define WAIT_TIME 4000000000 +#define MAX(a, b) ((a) > (b) ? (a) : (b)) +#define MIN(a, b) ((a) > (b) ? (a) : (b)) + +void ctrl_w( + int *const pos, + int *error, + struct timespec *last_back, + const char *text) +{ + if (*error) { + (*pos)++; + } + while (*pos > 0 && isspace(text[*pos - 1])) { + (*pos)--; + } + while (*pos > 0 && !isspace(text[*pos - 1])) { + (*pos)--; + } + *error = 0; + clock_gettime(CLOCK_REALTIME, last_back); +} + +void backspace( + int *const pos, + int *error, + struct timespec *last_back, + const char *text) +{ + if (*error) { + (*pos)++; + } + while (*pos > 0 && !isalpha(text[*pos - 1])) { + (*pos)--; + } + while (*pos > 0 && isalpha(text[*pos - 1])) { + (*pos)--; + } + *error = 0; + clock_gettime(CLOCK_REALTIME, last_back); +} + +void sync_state( + int sockfd, + int pos, + unsigned short *pos_to_word, + short length, + struct state *state_players, + char *num_players) +{ + unsigned short current_word = pos_to_word[pos]; + + struct state state = { + .current_word = current_word, + }; + + send(sockfd, &state, sizeof state, 0); + + recv(sockfd, num_players, 1, 0); + if (*num_players != 0) { + recv(sockfd, + state_players, + *num_players * sizeof(struct state), + 0); + } +} + +void input( + int c, + int sockfd, + int *const pos, + int *error, + struct timespec last_back, + struct timespec *last_input, + struct state *state_players, + char *num_players, + short *pos_to_word, + short length, + const char *text) +{ + struct timespec current; + clock_gettime(CLOCK_REALTIME, &current); + long diff = (current.tv_sec - last_back.tv_sec) * 1000000000 + + current.tv_nsec - last_back.tv_nsec; + + if (*error || diff < WAIT_TIME) { + return; + } + + if (c == text[*pos]) { + clock_gettime(CLOCK_REALTIME, last_input); + + if (isspace(text[*pos])) { + sync_state( + sockfd, + *pos, + pos_to_word, + length, + state_players, + num_players); + } + (*pos)++; + } else { + *error = 1; + } +} + +void print_text( + WINDOW *win, + int pos, + int error, + struct timespec last_back, + struct timespec last_input, + struct state *state_players, + char num_players, + char const *text, + unsigned short *pos_to_word, + short length, + short *spaces, + short num_spaces) +{ + struct timespec current; + clock_gettime(CLOCK_REALTIME, &current); + long diff_back = (current.tv_sec - last_back.tv_sec) * 1000000000 + + current.tv_nsec - last_back.tv_nsec; + long diff_input = (current.tv_sec - last_input.tv_sec) * 1000000000 + + current.tv_nsec - last_input.tv_nsec; + + unsigned short current_word = pos_to_word[pos]; + + int space_start = current_word + AHEAD_HIDE < num_spaces + ? current_word + AHEAD_HIDE + : num_spaces - 1; + int space_end = current_word + AHEAD_SHOW < num_spaces + ? current_word + AHEAD_SHOW + : num_spaces - 1; + int start = spaces[space_start]; + int end = spaces[space_end]; + + wmove(win, 0, 0); + wattron(win, A_DIM); + for (int i = 0; i < MAX(spaces[current_word], 0); i++) { + wprintw(win, "%c", text[i]); + } + wattroff(win, A_DIM); + for (int i = MAX(spaces[current_word], 0); i < start; i++) { + if (error) { + wattron(win, COLOR_PAIR(2)); + wprintw(win, "%c", text[i]); + wattroff(win, COLOR_PAIR(2)); + } else if (diff_back < WAIT_TIME) { + wattron(win, A_BOLD); + wattron(win, COLOR_PAIR(1)); + wprintw(win, "%c", text[i]); + wattroff(win, COLOR_PAIR(1)); + wattroff(win, A_BOLD); + } else if (diff_input < WAIT_TIME) { + wprintw(win, " "); + } else { + wattron(win, A_BOLD); + wprintw(win, "%c", text[i]); + wattroff(win, A_BOLD); + } + } + wattron(win, A_BOLD); + for (int i = start; i < end; i++) { + wprintw(win, "%c", text[i]); + } + wattroff(win, A_BOLD); + for (int i = end; i < length; i++) { + wprintw(win, "%c", text[i]); + } + + for (int i = 0; i < num_players; i++) { + wattron(win, COLOR_PAIR(2)); + int state_pos = spaces[state_players[i].current_word]; + if (state_pos >= 0) { + mvwprintw( + win, + state_pos / 58, + state_pos % 58, + "%c", + text[state_pos]); + } + wattroff(win, COLOR_PAIR(2)); + } + + for (int i = 0; i < num_players; i++) { + mvprintw(20, 0, "pos = %d\n", state_players[i].current_word); + } + refresh(); + + if (error) { + wmove(win, (pos + 1) / 58, (pos + 1) % 58); + } else { + wmove(win, pos / 58, pos % 58); + } + + wrefresh(win); +} + +int main(int argc, const char *argv[]) +{ + if (argc != 3) { + fprintf(stderr, "Usage: %s <host> <port>\n", argv[0]); + return 1; + } + + struct sockaddr_in addr = { + .sin_family = AF_INET, + }; + + int s; + if ((s = inet_pton(AF_INET, argv[1], &addr.sin_addr)) != 1) { + if (s == 0) { + fprintf(stderr, "Host is not in presentation format\n"); + } else { + perror("host"); + } + return 1; + } + + char *endptr; + long port = strtol(argv[2], &endptr, 10); + if (!(*argv[2] != '\0' && *endptr == '\0' && port < 65536)) { + fprintf(stderr, "ERROR: '%s' not a valid port.\n", argv[2]); + return 1; + } + addr.sin_port = htons(port); + + int sockfd = socket(AF_INET, SOCK_STREAM, 0); + int status = connect(sockfd, (struct sockaddr *)&addr, sizeof addr); + if (status == -1) { + perror("connect"); + endwin(); + exit(1); + } + + initscr(); + start_color(); + use_default_colors(); + noecho(); + init_pair(1, COLOR_RED, -1); + init_pair(2, -1, COLOR_RED); + + WINDOW *boxwin = newwin(15, 60, (LINES - 15) / 2, (COLS - 60) / 2); + WINDOW *win = subwin( + boxwin, 13, 58, (LINES - 15) / 2 + 1, (COLS - 60) / 2 + 1); + + struct state state_players[16]; + char num_players = 0; + + while (1) { + struct timespec last_back = {0, 0}; + struct timespec last_input = {0, 0}; + int error = 0; + + wclear(boxwin); + box(boxwin, 0, 0); + mvwprintw(win, 0, 0, "Waiting text from server..."); + refresh(); + wrefresh(boxwin); + wrefresh(win); + + char text[4096]; + int length = recv(sockfd, text, 4096, 0); + text[length] = '\0'; + + short spaces[1024]; + spaces[0] = -1; + short num_spaces = 1; + unsigned short pos_to_word[1024]; + for (int i = 0; i < length; i++) { + pos_to_word[i] = num_spaces - 1; + if (isspace(text[i])) { + spaces[num_spaces] = i + 1; + num_spaces++; + } + } + + spaces[num_spaces] = length; + num_spaces++; + + int pos = 0; + + print_text( + win, + pos, + error, + last_back, + last_input, + state_players, + num_players, + text, + pos_to_word, + length, + spaces, + num_spaces); + + timeout(100); + + while (pos < length) { + int c = getch(); + + if (c == ERR) { + } else if (c == 0x1b) { + c = getch(); + if (c == '\b' || c == 0x7f) { + backspace( + &pos, &error, &last_back, text); + } + } else if (c == 0x17) { + ctrl_w(&pos, &error, &last_back, text); + } else { + input(c, + sockfd, + &pos, + &error, + last_back, + &last_input, + state_players, + &num_players, + pos_to_word, + length, + text); + } + + print_text( + win, + pos, + error, + last_back, + last_input, + state_players, + num_players, + text, + pos_to_word, + length, + spaces, + num_spaces); + } + } + + endwin(); + close(sockfd); +} diff --git a/tt.h b/tt.h @@ -0,0 +1,7 @@ +#pragma once + +#define NAME_LENGHT 14 + +struct state { + unsigned short current_word; +}; diff --git a/ttserver.c b/ttserver.c @@ -0,0 +1,125 @@ +#include <sys/socket.h> +#include <netinet/in.h> +#include <stdio.h> +#include <unistd.h> +#include "tt.h" +#include <stdlib.h> +#include <json-c/json.h> +#include <string.h> + +int main(int argc, const char* argv[]) { + if (argc != 2) { + fprintf(stderr, "Usage: %s <port>\n", argv[0]); + return 1; + } + + char* endptr; + long port = strtol(argv[1], &endptr, 10); + if (!(*argv[1] != '\0' && *endptr == '\0' && port < 65536)) { + fprintf(stderr, "ERROR: '%s' is not a valid port.\n", argv[1]); + return 1; + } + + struct json_object *root = json_object_from_file("data/quotes_english.json"); + if (!root) { + fprintf(stderr, "Failed to parse JSON file.\n"); + return 1; + } + struct json_object *quotes; + json_object_object_get_ex(root, "quotes", &quotes); + size_t num_quotes = json_object_array_length(quotes); + + + int sockfd = socket(AF_INET, SOCK_STREAM, 0); + if (sockfd == -1) { + perror("socket"); + return 1; + } + + struct sockaddr_in address = { + .sin_family = AF_INET, + .sin_port = htons(port), + .sin_addr.s_addr = INADDR_ANY + }; + if (bind(sockfd, (struct sockaddr*) &address, sizeof address) == -1){ + perror("bind"); + return 1; + } + if (listen(sockfd, 5) == -1) { + perror("listen"); + return 1; + } + + fd_set set; + FD_ZERO(&set); + FD_SET(0, &set); + FD_SET(sockfd, &set); + + struct state states[FD_SETSIZE]; + + char running = 0; + + printf("\nChoose quote index: "); + fflush(stdout); + while (1) { + fd_set readfds = set; + if (select(FD_SETSIZE, &readfds, NULL, NULL, NULL) == -1) { + perror("select"); + return 1; + } + + if (FD_ISSET(0, &readfds)) { + char buffer[4096]; + read(0, buffer, 4096); + long quote_idx = strtol(buffer, &endptr, 10); + if (!((*buffer != '\n' && *buffer != '\0') && (*endptr == '\0' || *endptr =='\n'))) { + fprintf(stderr, "Error parsing quote index.\n"); + } else { + json_object *quote = json_object_array_get_idx(quotes, quote_idx); + json_object_object_get_ex(quote, "text", &quote); + const char *str_quote = json_object_get_string(quote); + for (int i = 0; i < FD_SETSIZE; i++) { + if (!(i != 0 && i != sockfd && FD_ISSET(i, &set))) { + continue; + } + send(i, str_quote, strlen(str_quote), 0); + } + } + printf("\nChoose quote index: "); + fflush(stdout); + + } + + if (FD_ISSET(sockfd, &readfds)) { + int fd = accept(sockfd, NULL, NULL); + FD_SET(fd, &set); + printf("player %d connected\n", fd); + } + + for (int fd = 0; fd < FD_SETSIZE; fd++) { + if (!(fd != 0 && fd != sockfd && FD_ISSET(fd, &readfds))) { + continue; + } + + int length = recv(fd, &states[fd], 16, 0); + if (length == 0) { + printf("Disconnected\n"); + FD_CLR(fd, &set); + continue; + } + + struct state buffer[FD_SETSIZE]; + unsigned short buffer_size = 0; + for (int i = 0; i < FD_SETSIZE; i++) { + if (!(i != 0 && i != fd && i != sockfd && FD_ISSET(i, &set))) { + continue; + } + buffer[buffer_size++] = states[i]; + } + send(fd, &buffer_size, 1, 0); + send(fd, buffer, buffer_size * (sizeof (struct state)), 0); + } + + } + return 0; +}