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 | +++++ |
| A | Makefile | | | 13 | +++++++++++++ |
| A | fetch.sh | | | 4 | ++++ |
| A | tt.c | | | 360 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | tt.h | | | 7 | +++++++ |
| A | ttserver.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, ¤t);
+ 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, ¤t);
+ 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", "es);
+ 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", "e);
+ 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;
+}