complete rewrite

This commit is contained in:
Nishi 2026-01-07 04:24:22 +09:00
commit 8a717a2b2e
Signed by: nishi
GPG key ID: 27EF69B208EB9343
41 changed files with 2625 additions and 1255 deletions

3
.gitmodules vendored
View file

@ -1,6 +1,3 @@
[submodule "external/libfishsoup"]
path = external/libfishsoup
url = ../libfishsoup
[submodule "external/milsko"]
path = external/milsko
url = https://gitea.nishi.boats/pyrite-dev/milsko

View file

@ -5,47 +5,9 @@ include(GNUInstallDirs)
set(CMAKE_INSTALL_RPATH "${CMAKE_INSTALL_FULL_LIBDIR}")
set(CMAKE_INSTALL_RPATH_USE_LINK_PATH TRUE)
add_subdirectory(external/libfishsoup)
add_subdirectory(external/milsko)
add_subdirectory(external/glad)
file(GLOB SERVER_SRC server/*.c src/*.c)
file(GLOB CLIENT_SRC client/*.c src/*.c)
add_executable(af_server ${SERVER_SRC} external/stb/stb_ds.c)
add_executable(af_client ${CLIENT_SRC} external/stb/stb_image.c)
add_executable(af_dumpfont tools/af_dumpfont.c external/stb/stb_image_write.c)
target_include_directories(af_server PRIVATE include external/stb ${CMAKE_CURRENT_BINARY_DIR})
target_include_directories(af_client PRIVATE include external/stb ${CMAKE_CURRENT_BINARY_DIR})
target_include_directories(af_dumpfont PRIVATE include external/stb ${CMAKE_CURRENT_BINARY_DIR})
target_compile_definitions(af_server PRIVATE AF_SERVER)
target_compile_definitions(af_client PRIVATE AF_CLIENT)
target_link_libraries(af_server PRIVATE fishsoup)
target_link_libraries(af_client PRIVATE fishsoup Mw glad)
if(CMAKE_SYSTEM_NAME STREQUAL "Windows")
target_link_libraries(af_client PRIVATE glu32)
else()
find_package(PkgConfig)
pkg_check_modules(GLU REQUIRED glu)
target_compile_options(af_client PRIVATE ${GLU_CFLAGS_OTHER})
target_include_directories(af_client PRIVATE ${GLU_INCLUDE_DIRS})
target_link_options(af_client PRIVATE ${GLU_LDFLAGS_OTHER})
target_link_directories(af_client PRIVATE ${GLU_LIBRARY_DIRS})
target_link_libraries(af_client PRIVATE ${GLU_LIBRARIES} m)
endif()
configure_file(include/af_config.h.in af_config.h)
install(
TARGETS af_server af_client af_dumpfont
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
)
add_subdirectory(engine)
install(
FILES resource/logo.png resource/font.png resource/shadow.vs resource/shadow.fs

View file

@ -1,298 +0,0 @@
#include <af_common.h>
#define SHADOW_WIDTH 1024
#define SHADOW_HEIGHT 1024
double gl_cam_lr = 0;
double gl_cam_ud = 0;
double gl_cam_x;
double gl_cam_y;
double gl_cam_z;
int gl_scene = AF_SCENE_MAIN;
GLuint gl_shadow;
static GLuint shadow_texture;
gl_scene_t gl_scenes[] = {
{gl_main_changed, gl_main_init, gl_main_draw, gl_main_after}, /**/
{NULL, NULL, NULL, NULL} /**/
};
static char gl_status[256];
void gl_resize(int width, int height) {
glViewport(0, 0, width, height);
}
void gl_init(void) {
GLfloat lightamb[] = {0.25, 0.25, 0.25, 1.0};
GLfloat lightcol[] = {1.0, 1.0, 1.0, 1.0};
glEnable(GL_DEPTH_TEST);
glEnable(GL_NORMALIZE);
glEnable(GL_BLEND);
glEnable(GL_COLOR_MATERIAL);
glEnable(GL_LIGHT0);
glEnable(GL_LIGHTING);
glEnable(GL_CULL_FACE);
glLightfv(GL_LIGHT0, GL_AMBIENT, lightamb);
glLightfv(GL_LIGHT0, GL_DIFFUSE, lightcol);
glLightfv(GL_LIGHT0, GL_SPECULAR, lightcol);
glLightModeli(GL_LIGHT_MODEL_LOCAL_VIEWER, GL_TRUE);
glColorMaterial(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE);
glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
gl_font_init();
gl_status[0] = 0;
glGenTextures(1, &shadow_texture);
glBindTexture(GL_TEXTURE_2D, shadow_texture);
glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT, SHADOW_WIDTH, SHADOW_HEIGHT, 0, GL_DEPTH_COMPONENT, GL_UNSIGNED_BYTE, 0);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_MODE, GL_COMPARE_R_TO_TEXTURE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_FUNC, GL_LEQUAL);
glTexParameteri(GL_TEXTURE_2D, GL_DEPTH_TEXTURE_MODE, GL_LUMINANCE);
glBindTexture(GL_TEXTURE_2D, 0);
if(gl_shader(&gl_shadow, DATAROOTDIR "/assaultfish/shadow.vs", DATAROOTDIR "/assaultfish/shadow.fs")) {
glUseProgram(gl_shadow);
glUniform1i(glGetUniformLocation(gl_shadow, "texture"), 1);
glUniform1i(glGetUniformLocation(gl_shadow, "texture_in"), 0);
}
}
void gl_cube(double x, double y, double z, double xs, double ys, double zs, double xr, double yr, double zr) {
int i;
glPushMatrix();
glTranslatef(x, y, z);
glScalef(xs, ys, zs);
glRotatef(xr, 1, 0, 0);
glRotatef(yr, 0, 1, 0);
glRotatef(zr, 0, 0, 1);
for(i = 0; i < 6; i++) {
glBegin(GL_QUADS);
glNormal3f(0, 0, 1);
glTexCoord2f(0, 0);
glVertex3f(-0.5, 0.5, 0.5);
glTexCoord2f(0, 1);
glVertex3f(-0.5, -0.5, 0.5);
glTexCoord2f(1, 1);
glVertex3f(0.5, -0.5, 0.5);
glTexCoord2f(1, 0);
glVertex3f(0.5, 0.5, 0.5);
glEnd();
if(i == 3) {
glRotatef(90, 1, 0, 0);
} else if(i == 4) {
glRotatef(180, 1, 0, 0);
} else {
glRotatef(90, 0, 1, 0);
}
}
glPopMatrix();
}
static void gl_common(void) {
char buf1[128];
char buf2[128];
int l;
gl_util_begin_2d();
glColor4f(0, 0, 0, 0.5);
sprintf(buf1, "AssaultFish %s", AF_VERSION);
sprintf(buf2, "%s", AF_COPYRIGHT);
l = gl_font_width(buf1);
if(l < gl_font_width(buf2)) l = gl_font_width(buf2);
glBegin(GL_QUADS);
glVertex2f(MwGetInteger(opengl, MwNwidth) - l, MwGetInteger(opengl, MwNheight) - gl_font_height(buf1) - gl_font_height(buf2));
glVertex2f(MwGetInteger(opengl, MwNwidth) - l, MwGetInteger(opengl, MwNheight));
glVertex2f(MwGetInteger(opengl, MwNwidth), MwGetInteger(opengl, MwNheight));
glVertex2f(MwGetInteger(opengl, MwNwidth), MwGetInteger(opengl, MwNheight) - gl_font_height(buf1) - gl_font_height(buf2));
glEnd();
glColor3f(1, 1, 1);
gl_font_text(buf1, MwGetInteger(opengl, MwNwidth) - gl_font_width(buf1), MwGetInteger(opengl, MwNheight) - gl_font_height(buf1) - gl_font_height(buf2), 1);
gl_font_text(buf2, MwGetInteger(opengl, MwNwidth) - gl_font_width(buf2), MwGetInteger(opengl, MwNheight) - gl_font_height(buf2), 1);
gl_font_text(gl_status, 0, 0, 1);
gl_util_end_2d();
}
static void scene(void) {
if(gl_scenes[gl_scene].draw != NULL) gl_scenes[gl_scene].draw();
}
void gl_render(void) {
GLfloat lightpos[4] = {2.5, 5, -2.5, 1};
GLint viewport[4];
GLdouble modelview[16];
GLdouble modelview2[16];
if(gl_scenes[gl_scene].init != NULL) gl_scenes[gl_scene].init();
glClear(GL_DEPTH_BUFFER_BIT);
glGetIntegerv(GL_VIEWPORT, &viewport[0]);
glViewport(0, 0, SHADOW_WIDTH, SHADOW_HEIGHT);
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
gluPerspective(60, (double)SHADOW_WIDTH / SHADOW_HEIGHT, 0.01, 100.0);
gluLookAt(lightpos[0], lightpos[1], lightpos[2], 0, 0, 0, 0, 1, 0);
glGetDoublev(GL_MODELVIEW_MATRIX, &modelview[0]);
glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE);
glDisable(GL_LIGHTING);
glCullFace(GL_FRONT);
scene();
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, shadow_texture);
glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 0, 0, SHADOW_WIDTH, SHADOW_HEIGHT);
glBindTexture(GL_TEXTURE_2D, 0);
glActiveTexture(GL_TEXTURE0);
glViewport(viewport[0], viewport[1], viewport[2], viewport[3]);
glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
glEnable(GL_LIGHTING);
glCullFace(GL_BACK);
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
gluPerspective(60, (double)MwGetInteger(opengl, MwNwidth) / MwGetInteger(opengl, MwNheight), 0.01, 100.0);
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
gluLookAt(gl_cam_x, gl_cam_y, gl_cam_z, gl_cam_x + cos(gl_cam_lr / 180.0 * M_PI), gl_cam_y + sin(gl_cam_ud / 180.0 * M_PI), gl_cam_z + sin(gl_cam_lr / 180.0 * M_PI), 0, 1, 0);
glGetDoublev(GL_MODELVIEW_MATRIX, &modelview2[0]);
glMatrixMode(GL_TEXTURE);
glLoadIdentity();
glTranslated(0.5, 0.5, 0.5);
glScaled(0.5, 0.5, 0.5);
glMultMatrixd(modelview);
glMultMatrixd(modelview2);
glMatrixMode(GL_MODELVIEW);
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, shadow_texture);
glEnable(GL_TEXTURE_2D);
glActiveTexture(GL_TEXTURE0);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glLightfv(GL_LIGHT0, GL_POSITION, lightpos);
scene();
glActiveTexture(GL_TEXTURE1);
glDisable(GL_TEXTURE_2D);
glBindTexture(GL_TEXTURE_2D, 0);
glActiveTexture(GL_TEXTURE0);
glMatrixMode(GL_TEXTURE);
glLoadIdentity();
glMatrixMode(GL_MODELVIEW);
gl_common();
if(gl_scenes[gl_scene].after != NULL) gl_scenes[gl_scene].after();
}
void gl_scene_change(void) {
if(gl_scenes[gl_scene].changed != NULL) gl_scenes[gl_scene].changed();
gl_status[0] = 0;
}
void gl_set_status(const char* status) {
strcpy(gl_status, status);
}
static void cross(double* out, double v00, double v01, double v02, double v10, double v11, double v12) {
out[0] = v01 * v12 - v02 * v11;
out[1] = v02 * v10 - v00 * v12;
out[2] = v00 * v11 - v01 * v10;
}
/*
* v0-v2
* | /
* v1/
*/
void gl_calc_normal3(double* out, double v00, double v01, double v02, double v10, double v11, double v12, double v20, double v21, double v22) {
double v[3];
double l = 0;
int i;
cross(&v[0], /**/
v10 - v00, v11 - v01, v12 - v02, /**/
v20 - v00, v21 - v01, v22 - v02 /**/
);
l = sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]);
for(i = 0; i < 3; i++) {
out[i] = v[i] / l;
}
}
/*
* v0-v3
* | |
* v1-v2
*/
void gl_calc_normal4(double* out, double v00, double v01, double v02, double v10, double v11, double v12, double v20, double v21, double v22, double v30, double v31, double v32) {
double v0[3];
double v1[3];
int i;
double l = 0;
gl_calc_normal3(&v0[0], v00, v01, v02, v10, v11, v12, v30, v31, v32);
gl_calc_normal3(&v1[0], v10, v11, v12, v20, v21, v22, v30, v31, v32);
for(i = 0; i < 3; i++) {
out[i] = v0[i] + v1[i];
}
l = sqrt(out[0] * out[0] + out[1] * out[1] + out[2] * out[2]);
for(i = 0; i < 3; i++) {
out[i] = out[i] / l;
}
}

View file

@ -1,60 +0,0 @@
#include <af_common.h>
#include <stb_image.h>
static GLuint font_atlas;
static int font_width = 0, font_height = 0;
void gl_font_init(void) {
unsigned int ch;
unsigned char* data = stbi_load(DATAROOTDIR "/assaultfish/font.png", &font_width, &font_height, &ch, 4);
glGenTextures(1, &font_atlas);
glBindTexture(GL_TEXTURE_2D, font_atlas);
if(data != NULL) {
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, font_width, font_height, 0, GL_RGBA, GL_UNSIGNED_BYTE, data);
free(data);
}
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glBindTexture(GL_TEXTURE_2D, 0);
}
void gl_font_text(const char* text, int x, int y, double scale) {
int i;
int gw = font_width / 16 * scale, gh = font_height / 16 * scale;
int is = glIsEnabled(GL_TEXTURE_2D) ? 1 : 0;
gl_util_begin_2d();
if(!is) glEnable(GL_TEXTURE_2D);
glBindTexture(GL_TEXTURE_2D, font_atlas);
glBegin(GL_QUADS);
for(i = 0; text[i] != 0; i++) {
double bx = 1.0 / 16 * (text[i] % 16), by = 1.0 / 16 * (text[i] / 16);
glTexCoord2f(bx, by);
glVertex2f(i * gw + x, y);
glTexCoord2f(bx + 1.0 / 16, by);
glVertex2f(i * gw + x + gw, y);
glTexCoord2f(bx + 1.0 / 16, by + 1.0 / 16);
glVertex2f(i * gw + x + gw, y + gh);
glTexCoord2f(bx, by + 1.0 / 16);
glVertex2f(i * gw + x, y + gh);
}
glEnd();
glBindTexture(GL_TEXTURE_2D, 0);
if(!is) glDisable(GL_TEXTURE_2D);
gl_util_end_2d();
}
int gl_font_width(const char* text) {
return font_width / 16 * strlen(text);
}
int gl_font_height(const char* text) {
return font_height / 16;
}

View file

@ -1,67 +0,0 @@
#include <af_common.h>
#include <stb_image.h>
static double rad;
void gl_main_changed(void) {
rad = 0;
}
void gl_main_init(void) {
gl_cam_x = 0;
gl_cam_y = 0;
gl_cam_z = -1;
gl_cam_lr = 90;
gl_cam_ud = 0;
}
void gl_main_draw(void) {
int i;
glUniform1f(glGetUniformLocation(gl_shadow, "solid_color"), 1);
glPushMatrix();
glRotatef(rad, 1, 0, 0);
glRotatef(rad, 0, 1, 0);
glRotatef(rad, 0, 0, 1);
for(i = 0; i < 8; i++) {
int c = (i % 2) == 0 ? 1 : 0;
double n[3];
double u = 0.3;
gl_calc_normal3(&n[0], 0, u * 1.4, 0, -u, 0, u, u, 0, u);
glColor3f(c ? 0.75 : 0, 0, c ? 0 : 0.75);
glBegin(GL_TRIANGLES);
glNormal3f(n[0], n[1], n[2]);
glVertex3f(0, u * 1.4, 0);
glVertex3f(-u, 0, u);
glVertex3f(u, 0, u);
glEnd();
glRotatef(90, 0, 1, 0);
if(i == 3) {
glRotatef(180, 0, 0, 1);
glRotatef(90, 0, 1, 0);
}
}
glPopMatrix();
glColor3f(1, 1, 1);
glBegin(GL_QUADS);
glNormal3f(0, 1, 0);
glVertex3f(-5, -1, -5);
glVertex3f(-5, -1, 5);
glVertex3f(5, -1, 5);
glVertex3f(5, -1, -5);
glEnd();
glUniform1f(glGetUniformLocation(gl_shadow, "solid_color"), 0);
}
void gl_main_after(void) {
rad += 60.0 / (1000.0 / AF_WAIT_MS);
}

View file

@ -1,108 +0,0 @@
#include <af_common.h>
static void err(const char* path, GLuint shader) {
GLint len, ret;
char* e;
glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &len);
e = malloc(len);
e[len] = 0;
glGetShaderInfoLog(shader, len, &ret, e);
fprintf(stderr, "%s: %s\n", path, e);
free(e);
}
int gl_shader(GLuint* r, const char* vs, const char* fs) {
GLuint vsi;
GLuint fsi;
FILE* f;
int vsl = 0;
int fsl = 0;
char* vss;
char* fss;
GLint st;
if((f = fopen(vs, "rb")) != NULL) {
fseek(f, 0, SEEK_END);
vsl = ftell(f);
fseek(f, 0, SEEK_SET);
vss = malloc(vsl + 1);
fread(vss, vsl, 1, f);
vss[vsl] = 0;
fclose(f);
} else {
return 0;
}
if((f = fopen(fs, "rb")) != NULL) {
fseek(f, 0, SEEK_END);
fsl = ftell(f);
fseek(f, 0, SEEK_SET);
fss = malloc(fsl + 1);
fread(fss, fsl, 1, f);
fss[fsl] = 0;
fclose(f);
} else {
free(vss);
return 0;
}
vsi = glCreateShader(GL_VERTEX_SHADER);
fsi = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(vsi, 1, (const GLchar**)&vss, NULL);
glShaderSource(fsi, 1, (const GLchar**)&fss, NULL);
glCompileShader(vsi);
glGetShaderiv(vsi, GL_COMPILE_STATUS, &st);
if(!st) {
err(vs, vsi);
glDeleteShader(vsi);
glDeleteShader(fsi);
free(vss);
free(fss);
return 0;
}
glCompileShader(fsi);
glGetShaderiv(fsi, GL_COMPILE_STATUS, &st);
if(!st) {
err(fs, fsi);
glDeleteShader(vsi);
glDeleteShader(fsi);
free(vss);
free(fss);
return 0;
}
free(vss);
free(fss);
*r = glCreateProgram();
glAttachShader(*r, vsi);
glAttachShader(*r, fsi);
glDeleteShader(vsi);
glDeleteShader(fsi);
glLinkProgram(*r);
glGetProgramiv(*r, GL_LINK_STATUS, &st);
if(!st) {
glDeleteProgram(*r);
return 0;
}
return 1;
}

View file

@ -1,34 +0,0 @@
#include <af_common.h>
void gl_util_begin_2d(void) {
glUseProgram(0);
glDisable(GL_DEPTH_TEST);
glDisable(GL_LIGHTING);
glDisable(GL_CULL_FACE);
glMatrixMode(GL_PROJECTION);
glPushMatrix();
glLoadIdentity();
glOrtho(0, MwGetInteger(opengl, MwNwidth), MwGetInteger(opengl, MwNheight), 0, -1, 1);
glMatrixMode(GL_MODELVIEW);
glPushMatrix();
glLoadIdentity();
}
void gl_util_end_2d(void) {
glMatrixMode(GL_MODELVIEW);
glPopMatrix();
glMatrixMode(GL_PROJECTION);
glPopMatrix();
glMatrixMode(GL_MODELVIEW);
glEnable(GL_CULL_FACE);
glEnable(GL_LIGHTING);
glEnable(GL_DEPTH_TEST);
glUseProgram(gl_shadow);
}

View file

@ -1,103 +0,0 @@
#include <af_common.h>
MwWidget root, window, opengl;
static const MwRect sizes[] = {
{0, 0, 640, 480},
{0, 0, 800, 600},
{0, 0, 1024, 768},
{0, 0, 1280, 1024},
};
static void resize(MwWidget handle, void* user, void* client) {
int ww = MwGetInteger(window, MwNwidth);
int wh = MwGetInteger(window, MwNheight);
MwVaApply(opengl,
MwNwidth, ww,
MwNheight, wh,
NULL);
gl_resize(ww, wh);
}
static void tick(MwWidget handle, void* user, void* client) {
net_poll();
gl_render();
MwOpenGLSwapBuffer(opengl);
}
static void* gl_load(const char* name) {
return MwOpenGLGetProcAddress(opengl, name);
}
void scene_change(int scene) {
gl_scene = scene;
gl_scene_change();
ui_scene_change();
}
int main(int argc, char** argv) {
MwSizeHints sh;
MwRect rc;
int i;
MwLibraryInit();
root = MwVaCreateWidget(NULL, "root", NULL, 0, 0, 0, 0,
MwNwaitMS, AF_WAIT_MS,
MwNbitmapFont, 0,
MwNmodernLook, 1,
NULL);
window = MwVaCreateWidget(MwWindowClass, "main", root, 0, 0, 640, 480,
MwNtitle, "AssaultFish",
NULL);
MwGetScreenSize(window, &rc);
for(i = sizeof(sizes) / sizeof(sizes[0]) - 1; i >= 0; i--) {
if(sizes[i].width < rc.width && sizes[i].height < rc.height) {
sh.min_width = sh.max_width = sizes[i].width;
sh.min_height = sh.max_height = sizes[i].height;
break;
}
}
for(i = 1; i < argc; i++) {
if(strcmp(argv[i], "--width") == 0 && (i + 1) < argc) {
sh.min_width = sh.max_width = atoi(argv[i + 1]);
} else if(strcmp(argv[i], "--height") == 0 && (i + 1) < argc) {
sh.min_height = sh.max_height = atoi(argv[i + 1]);
}
}
MwVaApply(window,
MwNsizeHints, &sh,
MwNx, (rc.width - sh.max_width) / 2,
MwNy, (rc.height - sh.max_height) / 2,
MwNwidth, sh.max_width,
MwNheight, sh.max_height,
NULL);
opengl = MwCreateWidget(MwOpenGLClass, "opengl", window, 0, 0, sh.max_width, sh.max_height);
MwOpenGLMakeCurrent(opengl);
gladLoadGLLoader(gl_load);
resize(window, NULL, NULL);
gl_init();
ui_init();
scene_change(AF_SCENE_MAIN);
MwAddUserHandler(window, MwNresizeHandler, resize, NULL);
MwAddUserHandler(root, MwNtickHandler, tick, NULL);
MwLoop(root);
}

View file

@ -1,107 +0,0 @@
#include <af_common.h>
static fishsoup_client_t* client = NULL;
enum OPAQUE_STATES {
OPAQUE_WAITING_HELLO = 0,
OPAQUE_JOINING
};
typedef struct opaque {
char username[256];
int state;
} opaque_t;
static void net_return(MwWidget handle, void* user, void* client) {
scene_change(AF_SCENE_MAIN);
}
static void on_packet(fishsoup_client_t* self, fishsoup_packet_t* pkt) {
opaque_t* o = self->user;
if(pkt->header.type == AF_PACKET_HELLO && pkt->header.length == sizeof(af_packet_hello_t) && o->state == OPAQUE_WAITING_HELLO) {
af_packet_hello_t* p_hello = pkt->data;
unsigned char mj, mi, pt;
af_common_version(&mj, &mi, &pt);
af_common_log_info("HELLO packet: Server version %hhu.%hhu.%hhu\n", p_hello->major, p_hello->minor, p_hello->patch);
if(mj == p_hello->major && mi >= p_hello->minor && (mi == p_hello->minor ? (pt >= p_hello->patch) : (1))) {
fishsoup_packet_t p;
af_packet_join_t p_join;
af_common_log_info("Server version is compatible with this client\n");
p.header.type = AF_PACKET_JOIN;
p.header.length = sizeof(p_join);
p.data = &p_join;
memcpy(p_join.username, o->username, 256);
fishsoup_packet_send(self->fd, &p);
o->state = OPAQUE_JOINING;
gl_set_status("Joining");
return;
}
af_common_log_info("Server version is incompatible with this client\n");
MwAddUserHandler(ui_message("Server version is incompatible with this client"), MwNactivateHandler, net_return, NULL);
fishsoup_client_destroy(self);
client = NULL;
} else if(pkt->header.type == AF_PACKET_OK && pkt->header.length == sizeof(af_packet_ok_t) && o->state == OPAQUE_JOINING) {
af_packet_ok_t* p_ok = pkt->data;
af_common_log_info("OK Packet: Joined successfully\n");
gl_set_status("Downloading data");
} else if(pkt->header.type == AF_PACKET_ERROR && pkt->header.length == sizeof(af_packet_error_t) && o->state == OPAQUE_JOINING) {
af_packet_error_t* p_error = pkt->data;
af_common_log_info("ERROR Packet: Failed to join\n");
fishsoup_client_destroy(self);
client = NULL;
}
}
static void on_disconnect(fishsoup_client_t* self) {
free(self->user);
}
void net_connect(const char* username, const char* hostname, int port) {
opaque_t* o;
scene_change(AF_SCENE_CONNECTING);
client = fishsoup_client(hostname, port == 0 ? AF_DEFAULT_PORT : port);
if(client == NULL) {
MwAddUserHandler(ui_message("Failed to connect"), MwNactivateHandler, net_return, NULL);
return;
}
o = client->user = malloc(sizeof(opaque_t));
o->state = OPAQUE_WAITING_HELLO;
memset(o->username, 0, 256);
memcpy(o->username, username, strlen(username));
fishsoup_client_on_packet(client, on_packet);
fishsoup_client_on_disconnect(client, on_disconnect);
gl_set_status("Waiting for server to be ready");
}
void net_poll(void) {
int ret;
if(client == NULL) return;
while(client != NULL && (ret = fishsoup_client_poll(client)) > 0);
if(ret < 0) {
MwAddUserHandler(ui_message("Connection destroyed"), MwNactivateHandler, net_return, NULL);
fishsoup_client_destroy(client);
client = NULL;
return;
}
}

View file

@ -1,125 +0,0 @@
#include <af_common.h>
MwWidget ui_notice(int width, int height) {
if(width == 0 && height == 0) {
width = 400;
height = 300;
}
return MwVaCreateWidget(MwFrameClass, "notice", window, (MwGetInteger(window, MwNwidth) - width) / 2, (MwGetInteger(window, MwNheight) - height) / 2, width, height,
MwNhasBorder, 1,
MwNinverted, 0,
NULL);
}
void ui_notice_destroy(MwWidget widget) {
MwDestroyWidget(widget);
}
static void ui_login_join(MwWidget handle, void* user, void* client) {
char* username = MwStringDuplicate(MwGetText(MwGetVoid(user, "VusernameEntry"), MwNtext));
char* hostname = MwStringDuplicate(MwGetText(MwGetVoid(user, "VhostnameEntry"), MwNtext));
int port = atoi(MwGetText(MwGetVoid(user, "VportEntry"), MwNtext));
MwLLDestroyPixmap(MwGetVoid(user, MwNpixmap));
ui_notice_destroy(user);
net_connect(username, hostname, port);
free(username);
free(hostname);
}
static void ui_login(void) {
MwWidget wnd = ui_notice(0, 0);
int bw = MwDefaultBorderWidth(wnd);
MwWidget join = MwVaCreateWidget(MwButtonClass, "join", wnd, MwGetInteger(wnd, MwNwidth) - bw - 10 - 96, MwGetInteger(wnd, MwNheight) - bw - 10 - 24, 96, 24,
MwNtext, "Join",
NULL);
MwWidget box = MwVaCreateWidget(MwBoxClass, "box", wnd, bw, bw, MwGetInteger(wnd, MwNwidth) - bw * 2, MwGetInteger(wnd, MwNheight) - bw * 2 - 10 - 24,
MwNorientation, MwVERTICAL,
MwNmargin, 10,
MwNpadding, 10,
NULL);
MwWidget icon = MwVaCreateWidget(MwImageClass, "icon", box, 0, 0, 0, 0,
MwNfillArea, 0,
MwNfixedSize, 100,
NULL);
MwWidget entrybox[3];
MwWidget entryinput[3];
MwLLPixmap px = MwLoadImage(wnd, DATAROOTDIR "/assaultfish/logo.png");
const char* labels[3] = {
"Username",
"Hostname",
"Port"};
const char* entries[3] = {
"JohnDoe",
"127.0.0.1",
"",
};
int i;
char buf[16];
sprintf(buf, "%d", AF_DEFAULT_PORT);
entries[2] = buf;
for(i = 0; i < 3; i++) {
entrybox[i] = MwVaCreateWidget(MwBoxClass, "entrybox", box, 0, 0, 0, 0,
MwNfixedSize, 24,
MwNmargin, 10,
NULL);
MwVaCreateWidget(MwLabelClass, "label", entrybox[i], 0, 0, 0, 0,
MwNfixedSize, 100,
MwNtext, labels[i],
MwNalignment, MwALIGNMENT_END,
NULL);
entryinput[i] = MwVaCreateWidget(MwEntryClass, "entry", entrybox[i], 0, 0, 0, 0,
MwNtext, entries[i],
NULL);
}
MwVaApply(icon,
MwNpixmap, px,
NULL);
MwVaApply(wnd,
MwNpixmap, px,
"VusernameEntry", entryinput[0],
"VhostnameEntry", entryinput[1],
"VportEntry", entryinput[2],
NULL);
MwAddUserHandler(join, MwNactivateHandler, ui_login_join, wnd);
}
static void ui_message_ok(MwWidget handle, void* user, void* client) {
MwDispatchUserHandler(user, MwNactivateHandler, NULL);
ui_notice_destroy(user);
}
MwWidget ui_message(const char* message) {
MwWidget wnd = ui_notice(0, 0);
int bw = MwDefaultBorderWidth(wnd);
MwWidget label = MwVaCreateWidget(MwLabelClass, "label", wnd, bw, (MwGetInteger(wnd, MwNheight) - 24) / 2, MwGetInteger(wnd, MwNwidth) - bw * 2, 24,
MwNtext, message,
NULL);
MwWidget ok = MwVaCreateWidget(MwButtonClass, "ok", wnd, MwGetInteger(wnd, MwNwidth) - bw - 10 - 96, MwGetInteger(wnd, MwNheight) - bw - 10 - 24, 96, 24,
MwNtext, "OK",
NULL);
MwAddUserHandler(ok, MwNactivateHandler, ui_message_ok, wnd);
return wnd;
}
void ui_scene_change(void) {
if(gl_scene == AF_SCENE_MAIN) {
ui_login();
}
}
void ui_init(void) {
MwToggleDarkTheme(root, 1);
}

11
engine/CMakeLists.txt Normal file
View file

@ -0,0 +1,11 @@
cmake_minimum_required(VERSION 3.11)
project(assaultfish)
include(GNUInstallDirs)
set(CMAKE_INSTALL_RPATH "${CMAKE_INSTALL_FULL_LIBDIR}")
set(CMAKE_INSTALL_RPATH_USE_LINK_PATH TRUE)
add_subdirectory(external/glad)
add_subdirectory(net)
add_subdirectory(tools)

3
engine/README.txt Normal file
View file

@ -0,0 +1,3 @@
GearBox game engine source code
Licensed under 3-clause BSD

20
engine/net/.clang-format Normal file
View file

@ -0,0 +1,20 @@
---
Language: Cpp
UseTab: Always
TabWidth: 8
AlignConsecutiveAssignments:
Enabled: true
AlignConsecutiveDeclarations:
Enabled: true
IndentWidth: 8
PointerAlignment: Left
ColumnLimit: 0
AllowShortIfStatementsOnASingleLine: Always
AllowShortBlocksOnASingleLine: Never
AllowShortFunctionsOnASingleLine: Empty
AllowShortLoopsOnASingleLine: true
SpaceBeforeParens: Never
AlignEscapedNewlines: DontAlign
SortIncludes: false
AllowShortEnumsOnASingleLine: false
#IndentPPDirectives: AfterHash

21
engine/net/CMakeLists.txt Normal file
View file

@ -0,0 +1,21 @@
cmake_minimum_required(VERSION 3.11)
project(libfishsoup)
add_library(fishsoup SHARED src/client.c src/server.c src/socket.c src/stb_ds.c)
target_include_directories(fishsoup PUBLIC include)
find_package(PkgConfig)
pkg_check_modules(ZLIB REQUIRED zlib)
target_compile_options(fishsoup PRIVATE ${ZLIB_CFLAGS_OTHER})
target_include_directories(fishsoup PRIVATE ${ZLIB_INCLUDE_DIRS})
target_link_options(fishsoup PRIVATE ${ZLIB_LDFLAGS_OTHER})
target_link_directories(fishsoup PRIVATE ${ZLIB_LIBRARY_DIRS})
target_link_libraries(fishsoup PRIVATE ${ZLIB_LIBRARIES})
include(GNUInstallDirs)
install(
TARGETS fishsoup
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
)

24
engine/net/LICENSE Normal file
View file

@ -0,0 +1,24 @@
Copyright (c) 2025, Fishsoup
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of the <organization> nor the names of its contributors
may be used to endorse or promote products derived from this software
without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

33
engine/net/Makefile Normal file
View file

@ -0,0 +1,33 @@
CC = cc
CFLAGS = -g -Iinclude -fPIC `pkg-config --cflags zlib`
LDFLAGS = -shared
LIBS = `pkg-config --libs zlib`
OBJS = src/client.o src/server.o src/socket.o src/stb_ds.o
LIB = lib
SO = .so
.PHONY: all format clean
.SUFFIXES: .c .o
all: $(LIB)fishsoup$(SO)
format:
clang-format --verbose -i `find src include "(" -name "*.c" -or -name "*.h" ")" -and -not -name "stb_*.h"` test*.c
$(LIB)fishsoup$(SO): $(OBJS)
$(CC) $(LDFLAGS) -o $@ $(OBJS) $(LIBS)
testclient: testclient.c $(LIB)fishsoup$(SO)
cc -o $@ testclient.c -Wl,-R./ -L./ -Iinclude -lfishsoup
testserver: testserver.c $(LIB)fishsoup$(SO)
cc -o $@ testserver.c -Wl,-R./ -L./ -Iinclude -lfishsoup
.c.o:
$(CC) $(CFLAGS) -c -o $@ $<
clean:
rm -f *$(SO) $(OBJS) ./testserver ./testclient

View file

@ -0,0 +1,86 @@
#ifndef __FISHSOUP_H__
#define __FISHSOUP_H__
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <time.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <netinet/tcp.h>
#include <poll.h>
/* TCP */
#define FISHSOUP_PORT 3219
#define FISHSOUP_PING 3
enum fishsoup_packet_type {
FISHSOUP_PACKET_PING = 0,
FISHSOUP_PACKET_PONG,
FISHSOUP_PACKET_USER
};
typedef struct fishsoup_client fishsoup_client_t;
typedef struct fishsoup_server fishsoup_server_t;
typedef struct fishsoup_packet_header fishsoup_packet_header_t;
typedef struct fishsoup_packet fishsoup_packet_t;
struct fishsoup_client {
int fd;
time_t last_ping;
void (*on_packet)(fishsoup_client_t* client, fishsoup_packet_t* packet);
void (*on_disconnect)(fishsoup_client_t* client);
void* user;
struct {
int sent_ping;
int cleanup;
} server;
};
struct fishsoup_server {
int fd;
struct sockaddr_in address;
fishsoup_client_t* clients;
void* user;
void (*on_client)(fishsoup_server_t* server, fishsoup_client_t* client);
void (*on_tick)(fishsoup_server_t* server);
};
#pragma pack(1)
struct fishsoup_packet_header {
unsigned char type;
unsigned short length; /* ok, seriously, who sends 64k at ONCE */
};
#pragma pack()
/* !!! REMEMBER TO FREE DATA AFTER RECV UNLESS IT'S NULL !!! */
struct fishsoup_packet {
fishsoup_packet_header_t header;
void* data;
};
fishsoup_client_t* fishsoup_client(const char* host, int port);
void fishsoup_client_on_packet(fishsoup_client_t* client, void (*on_packet)(fishsoup_client_t* client, fishsoup_packet_t* packet));
void fishsoup_client_on_disconnect(fishsoup_client_t* client, void (*on_disconnect)(fishsoup_client_t* client));
int fishsoup_client_poll(fishsoup_client_t* client);
void fishsoup_client_destroy(fishsoup_client_t* client);
fishsoup_server_t* fishsoup_server(int port);
int fishsoup_server_clients(fishsoup_server_t* server);
void fishsoup_server_loop(fishsoup_server_t* server);
void fishsoup_server_on_client(fishsoup_server_t* server, void (*on_client)(fishsoup_server_t* server, fishsoup_client_t* client));
void fishsoup_server_on_tick(fishsoup_server_t* server, void (*on_tick)(fishsoup_server_t* server));
void fishsoup_server_broadcast(fishsoup_server_t* server, fishsoup_packet_t* packet);
void fishsoup_server_destroy(fishsoup_server_t* server);
int fishsoup_fd_send(int fd, const void* buf, int length);
int fishsoup_fd_recv(int fd, void* buf, int length, int to);
int fishsoup_packet_send(int fd, fishsoup_packet_t* packet);
int fishsoup_packet_recv(int fd, fishsoup_packet_t* packet);
#endif

93
engine/net/src/client.c Normal file
View file

@ -0,0 +1,93 @@
#include <fishsoup.h>
fishsoup_client_t* fishsoup_client(const char* host, int port) {
fishsoup_client_t* client = malloc(sizeof(*client));
struct addrinfo hints;
char p[32];
struct addrinfo* result;
struct addrinfo* rp;
if(port == 0) port = FISHSOUP_PORT;
sprintf(p, "%d", port);
memset(&hints, 0, sizeof(hints));
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
if(getaddrinfo(host, p, &hints, &result) != 0) {
free(client);
return NULL;
}
for(rp = result; rp != NULL; rp = rp->ai_next) {
int val = 1;
if((client->fd = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol)) < 0) {
continue;
}
val = 1;
setsockopt(client->fd, IPPROTO_TCP, TCP_NODELAY, (char*)&val, sizeof(val));
if(connect(client->fd, rp->ai_addr, rp->ai_addrlen) >= 0) {
break;
}
close(client->fd);
}
if(rp == NULL) {
free(client);
return NULL;
}
client->on_packet = NULL;
client->on_disconnect = NULL;
return client;
}
void fishsoup_client_on_packet(fishsoup_client_t* client, void (*on_packet)(fishsoup_client_t* client, fishsoup_packet_t* packet)) {
client->on_packet = on_packet;
}
void fishsoup_client_on_disconnect(fishsoup_client_t* client, void (*on_disconnect)(fishsoup_client_t* client)) {
client->on_disconnect = on_disconnect;
}
int fishsoup_client_poll(fishsoup_client_t* client) {
struct pollfd pfd;
fishsoup_packet_t pkt;
int ret;
pfd.fd = client->fd;
pfd.events = POLLIN | POLLPRI;
ret = poll(&pfd, 1, 0);
if(ret <= 0) return ret;
ret = fishsoup_packet_recv(client->fd, &pkt);
if(ret == 0) return -1; /* connection broke, probably */
if(pkt.header.type == FISHSOUP_PACKET_PING) {
fishsoup_packet_t ret;
client->last_ping = time(NULL);
ret.header.type = FISHSOUP_PACKET_PONG;
ret.header.length = 0;
ret.data = NULL;
if(fishsoup_packet_send(client->fd, &ret) == 0) {
if(pkt.data != NULL) free(pkt.data);
return -1;
}
}
if(client->on_packet != NULL) client->on_packet(client, &pkt);
if(pkt.data != NULL) free(pkt.data);
return 1;
}
void fishsoup_client_destroy(fishsoup_client_t* client) {
if(client->on_disconnect != NULL) client->on_disconnect(client);
close(client->fd);
free(client);
}

199
engine/net/src/server.c Normal file
View file

@ -0,0 +1,199 @@
#include <fishsoup.h>
#include "stb_ds.h"
fishsoup_server_t* fishsoup_server(int port) {
fishsoup_server_t* server = malloc(sizeof(*server));
int val;
if(port == 0) port = FISHSOUP_PORT;
if((server->fd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0) {
free(server);
return NULL;
}
val = 1;
setsockopt(server->fd, IPPROTO_TCP, TCP_NODELAY, (char*)&val, sizeof(val));
server->address.sin_family = AF_INET;
server->address.sin_addr.s_addr = INADDR_ANY;
server->address.sin_port = htons(port);
if(bind(server->fd, (struct sockaddr*)&server->address, sizeof(server->address)) < 0) {
close(server->fd);
free(server);
return NULL;
}
if(listen(server->fd, 128) < 0) {
close(server->fd);
free(server);
return NULL;
}
server->clients = NULL;
server->on_client = NULL;
server->on_tick = NULL;
return server;
}
int fishsoup_server_clients(fishsoup_server_t* server) {
return arrlen(server->clients);
}
void fishsoup_server_loop(fishsoup_server_t* server) {
struct pollfd* pfd;
while(1) {
int i;
int ret;
pfd = malloc(sizeof(*pfd) * (1 + arrlen(server->clients)));
pfd[0].fd = server->fd;
pfd[0].events = POLLIN | POLLPRI;
for(i = 0; i < arrlen(server->clients); i++) {
pfd[1 + i].fd = server->clients[i].fd;
pfd[1 + i].events = POLLIN | POLLPRI;
}
ret = poll(pfd, 1 + arrlen(server->clients), 50);
if(ret < 0) {
free(pfd);
break;
}
if(server->on_tick != NULL) server->on_tick(server);
if(pfd[0].revents & POLLIN) {
fishsoup_client_t client;
struct sockaddr_in address;
int len = sizeof(address);
client.fd = accept(server->fd, (struct sockaddr*)&address, &len);
client.last_ping = time(NULL);
client.server.cleanup = 0;
client.server.sent_ping = 0;
client.on_packet = NULL;
arrput(server->clients, client);
if(server->on_client != NULL) server->on_client(server, &server->clients[arrlen(server->clients) - 1]);
#ifdef DEBUG
fprintf(stderr, "Client %d accepted\n", server->clients[i].fd);
#endif
}
for(i = 0; i < arrlen(server->clients); i++) {
time_t diff;
if(server->clients[i].server.cleanup) continue;
if(pfd[1 + i].revents & POLLIN) {
fishsoup_packet_t pkt;
if(fishsoup_packet_recv(server->clients[i].fd, &pkt) == 0) {
server->clients[i].server.cleanup = 1;
} else {
if(pkt.header.type == FISHSOUP_PACKET_PONG && server->clients[i].server.sent_ping) {
server->clients[i].last_ping = time(NULL);
server->clients[i].server.sent_ping = 0;
#ifdef DEBUG
fprintf(stderr, "Pong from client %d\n", server->clients[i].fd);
#endif
} else if(pkt.header.type == FISHSOUP_PACKET_PING) {
fishsoup_packet_t ret;
ret.header.type = FISHSOUP_PACKET_PONG;
ret.header.length = 0;
ret.data = NULL;
if(fishsoup_packet_send(server->clients[i].fd, &ret) == 0) {
server->clients[i].server.cleanup = 1;
} else {
#ifdef DEBUG
fprintf(stderr, "Ping from client %d, sending back pong\n", server->clients[i].fd);
#endif
}
}
if(!server->clients[i].server.cleanup && server->clients[i].on_packet != NULL) server->clients[i].on_packet(&server->clients[i], &pkt);
if(pkt.data != NULL) free(pkt.data);
}
}
if(server->clients[i].server.cleanup) continue;
diff = time(NULL) - server->clients[i].last_ping;
if(diff == FISHSOUP_PING && !server->clients[i].server.sent_ping) {
fishsoup_packet_t pkt;
pkt.header.type = FISHSOUP_PACKET_PING;
pkt.header.length = 0;
pkt.data = NULL;
if(fishsoup_packet_send(server->clients[i].fd, &pkt) == 0) {
server->clients[i].server.cleanup = 1;
#ifdef DEBUG
fprintf(stderr, "Failed to ping client %d\n", server->clients[i].fd);
#endif
} else {
server->clients[i].server.sent_ping = 1;
#ifdef DEBUG
fprintf(stderr, "Pinging client %d\n", server->clients[i].fd);
#endif
}
} else if(diff == FISHSOUP_PING * 2 && server->clients[i].server.sent_ping) {
server->clients[i].server.cleanup = 1;
#ifdef DEBUG
fprintf(stderr, "Ping timeout (client %d)\n", server->clients[i].fd);
#endif
}
}
for(i = 0; i < arrlen(server->clients); i++) {
if(server->clients[i].server.cleanup) {
#ifdef DEBUG
fprintf(stderr, "Client %d cleanup\n", server->clients[i].fd);
#endif
if(server->clients[i].on_disconnect != NULL) server->clients[i].on_disconnect(&server->clients[i]);
close(server->clients[i].fd);
arrdel(server->clients, i);
i--;
}
}
free(pfd);
}
}
void fishsoup_server_on_client(fishsoup_server_t* server, void (*on_client)(fishsoup_server_t* server, fishsoup_client_t* client)) {
server->on_client = on_client;
}
void fishsoup_server_on_tick(fishsoup_server_t* server, void (*on_tick)(fishsoup_server_t* server)) {
server->on_tick = on_tick;
}
void fishsoup_server_destroy(fishsoup_server_t* server) {
int i;
for(i = 0; i < arrlen(server->clients); i++) {
if(server->clients[i].on_disconnect != NULL) server->clients[i].on_disconnect(&server->clients[i]);
close(server->clients[i].fd);
}
arrfree(server->clients);
close(server->fd);
free(server);
}
void fishsoup_server_broadcast(fishsoup_server_t* server, fishsoup_packet_t* packet) {
int i;
for(i = 0; i < arrlen(server->clients); i++) {
if(fishsoup_packet_send(server->clients[i].fd, packet) == 0) {
server->clients[i].server.cleanup = 1;
}
}
}

200
engine/net/src/socket.c Normal file
View file

@ -0,0 +1,200 @@
#include <fishsoup.h>
#include <zlib.h>
#define CHUNK 512
int fishsoup_packet_send(int fd, fishsoup_packet_t* packet) {
int r;
fishsoup_packet_header_t h;
int len = 0;
unsigned char* data = NULL;
if(packet->header.length > 0) {
z_stream strm;
int in = packet->header.length;
int seek = 0;
int flush = 0;
unsigned char buf[CHUNK];
strm.zalloc = Z_NULL;
strm.zfree = Z_NULL;
strm.opaque = Z_NULL;
if(deflateInit(&strm, Z_DEFAULT_COMPRESSION) != Z_OK) return 0;
do {
strm.avail_in = in <= CHUNK ? in : CHUNK;
strm.next_in = ((unsigned char*)packet->data) + seek;
flush = in <= CHUNK ? Z_FINISH : Z_NO_FLUSH;
do {
int have;
strm.avail_out = CHUNK;
strm.next_out = buf;
deflate(&strm, flush);
have = CHUNK - strm.avail_out;
if(data == NULL) {
data = malloc(have);
memcpy(data, buf, have);
} else {
unsigned char* old = data;
data = malloc(len + have);
memcpy(data, old, len);
memcpy(data + len, buf, have);
free(data);
}
len += have;
} while(strm.avail_out == 0);
seek += CHUNK;
} while(flush != Z_FINISH);
deflateEnd(&strm);
}
memcpy(&h, &packet->header, sizeof(h));
h.length = htons(len);
r = fishsoup_fd_send(fd, &h, sizeof(h));
if(r != sizeof(h)) return 0;
if(data != NULL) {
r = fishsoup_fd_send(fd, data, len);
if(r != sizeof(h)) {
free(data);
return 0;
}
free(data);
}
return sizeof(h) + len;
}
int fishsoup_packet_recv(int fd, fishsoup_packet_t* packet) {
fishsoup_packet_header_t h;
if(fishsoup_fd_recv(fd, &h, sizeof(h), 500) != sizeof(h)) {
packet->data = NULL;
return 0;
}
memcpy(&packet->header, &h, sizeof(h));
packet->header.length = ntohs(packet->header.length);
packet->data = NULL;
if(packet->header.length > 0) {
unsigned char* data = malloc(packet->header.length);
z_stream strm;
int in = packet->header.length;
int ret;
int seek = 0;
int len = 0;
unsigned char buf[CHUNK];
if(fishsoup_fd_recv(fd, data, packet->header.length, 50) != packet->header.length) {
free(data);
return 0;
}
strm.zalloc = Z_NULL;
strm.zfree = Z_NULL;
strm.opaque = Z_NULL;
strm.avail_in = 0;
strm.next_in = Z_NULL;
if(inflateInit(&strm) != Z_OK) {
free(data);
return 0;
}
do {
strm.avail_in = in <= CHUNK ? in : CHUNK;
strm.next_in = data + seek;
do {
int have;
strm.avail_out = CHUNK;
strm.next_out = buf;
ret = inflate(&strm, Z_NO_FLUSH);
if(ret == Z_NEED_DICT || ret == Z_DATA_ERROR || ret == Z_MEM_ERROR) {
inflateEnd(&strm);
if(packet->data != NULL) {
free(packet->data);
packet->data = NULL;
}
free(data);
return 0;
}
have = CHUNK - strm.avail_out;
if(packet->data == NULL) {
packet->data = malloc(have);
memcpy(packet->data, buf, have);
} else {
unsigned char* old = packet->data;
packet->data = malloc(len + have);
memcpy(packet->data, old, len);
memcpy(packet->data + len, buf, have);
free(packet->data);
}
len += have;
} while(strm.avail_out == 0);
} while(ret != Z_STREAM_END);
inflateEnd(&strm);
free(data);
packet->header.length = len;
}
return sizeof(h) + packet->header.length;
}
int fishsoup_fd_send(int fd, const void* buf, int length) {
struct pollfd pfd;
pfd.fd = fd;
pfd.events = POLLOUT | POLLPRI;
poll(&pfd, 1, 0);
if(!(pfd.revents & POLLOUT)) return 0;
return send(fd, buf, length, 0);
}
int fishsoup_fd_recv(int fd, void* buf, int length, int to) {
struct pollfd pfd;
int ret;
unsigned char* b = buf;
int i = 0;
time_t t = time(NULL);
pfd.fd = fd;
pfd.events = POLLIN | POLLPRI;
while(i < length) {
/* i think 500 is reasonable timeout. no? */
ret = poll(&pfd, 1, to);
if((time(NULL) - t) >= 5) return i; /* we waited enough! kill the connection */
if(ret <= 0) return i;
ret = recv(fd, &b[i], 1, 0);
if(ret < 1) return i;
i++;
}
return i;
}

View file

@ -1,2 +1,2 @@
#define STB_DS_IMPLEMENTATION
#include <stb_ds.h>
#include "stb_ds.h"

1895
engine/net/src/stb_ds.h Normal file

File diff suppressed because it is too large Load diff

11
engine/net/testclient.c Normal file
View file

@ -0,0 +1,11 @@
#include <fishsoup.h>
void on(fishsoup_client_t* client, fishsoup_packet_t* pkt) {
if(pkt->header.type == FISHSOUP_PACKET_USER) printf("Received: %s\n", pkt->data);
}
int main() {
fishsoup_client_t* client = fishsoup_client("127.0.0.1", 0);
fishsoup_client_on_packet(client, on);
while(fishsoup_client_poll(client) >= 0);
}

18
engine/net/testserver.c Normal file
View file

@ -0,0 +1,18 @@
#include <fishsoup.h>
void on(fishsoup_server_t* server, fishsoup_client_t* client) {
fishsoup_packet_t pkt;
pkt.data = "Hello world!";
pkt.header.length = strlen(pkt.data) + 1;
pkt.header.flag = 0;
pkt.header.type = FISHSOUP_PACKET_USER;
fishsoup_packet_send(client->fd, &pkt);
}
int main() {
fishsoup_server_t* server = fishsoup_server(0);
fishsoup_server_on_client(server, on);
fishsoup_server_loop(server);
}

View file

@ -0,0 +1,9 @@
include(GNUInstallDirs)
macro(compile_tool name)
file(GLOB ${name}_SRC ${name}/*.c)
add_executable(${name} ${${name}_SRC})
target_include_directories(${name} PRIVATE ../../external/stb)
endmacro()
compile_tool(gb_dumpfont)

@ -1 +0,0 @@
Subproject commit b3ae87cf7e3e8244ec6d48f983af3e575ecd39bd

View file

@ -1,2 +0,0 @@
#define STB_IMAGE_IMPLEMENTATION
#include <stb_image.h>

View file

@ -1,77 +0,0 @@
#ifndef __AF_CLIENT_H__
#define __AF_CLIENT_H__
#include <glad/glad.h>
#include <Mw/Milsko.h>
#include <Mw/Widget/OpenGL.h>
#include <GL/glu.h>
#define AF_WAIT_MS 15
enum AF_SCENES {
AF_SCENE_MAIN = 0,
AF_SCENE_CONNECTING
};
/* main.c */
extern MwWidget root, window, opengl;
void scene_change(int scene);
/* gl.c */
typedef struct gl_scene {
void (*changed)(void);
void (*init)(void);
void (*draw)(void);
void (*after)(void);
} gl_scene_t;
extern int gl_scene;
extern gl_scene_t gl_scenes[];
extern double gl_cam_lr;
extern double gl_cam_ud;
extern double gl_cam_x;
extern double gl_cam_y;
extern double gl_cam_z;
extern GLuint gl_shadow;
void gl_init(void);
void gl_render(void);
void gl_resize(int width, int height);
void gl_scene_change(void);
void gl_calc_normal3(double* out, double v00, double v01, double v02, double v10, double v11, double v12, double v20, double v21, double v22);
void gl_calc_normal4(double* out, double v00, double v01, double v02, double v10, double v11, double v12, double v20, double v21, double v22, double v30, double v31, double v32);
void gl_set_status(const char* status);
/* gl_font.c */
void gl_font_init(void);
void gl_font_text(const char* text, int x, int y, double scale);
int gl_font_width(const char* text);
int gl_font_height(const char* text);
/* gl_main.c */
void gl_main_changed(void);
void gl_main_init(void);
void gl_main_draw(void);
void gl_main_after(void);
/* gl_util.c */
void gl_util_begin_2d(void);
void gl_util_end_2d(void);
/* gl_shader.c */
int gl_shader(GLuint* r, const char* vs, const char* fs);
/* net.c */
void net_connect(const char* username, const char* hostname, int port);
void net_poll(void);
/* ui.c */
MwWidget ui_notice(int width, int height);
void ui_notice_destroy(MwWidget widget);
void ui_init(void);
void ui_scene_change(void);
MwWidget ui_message(const char* message);
#endif

View file

@ -1,59 +0,0 @@
#ifndef __AF_COMMON_H__
#define __AF_COMMON_H__
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdarg.h>
#include <time.h>
#include <fishsoup.h>
#include <af_config.h>
#ifdef AF_SERVER
#include <af_server.h>
#endif
#ifdef AF_CLIENT
#include <af_client.h>
#endif
#define AF_DEFAULT_PORT 5312
#define AF_VERSION "0.0.0"
#define AF_COPYRIGHT "Copyright (C) 2025-2026 Pyrite development team"
enum AF_USER_PACKETS {
AF_PACKET_HELLO = FISHSOUP_PACKET_USER,
AF_PACKET_OK,
AF_PACKET_ERROR,
AF_PACKET_JOIN
};
#pragma pack(1)
typedef struct af_packet_hello {
unsigned char major;
unsigned char minor;
unsigned char patch;
} af_packet_hello_t;
typedef struct af_packet_ok {
char reason[256];
} af_packet_ok_t;
typedef struct af_packet_error {
char reason[256];
} af_packet_error_t;
typedef struct af_packet_join {
char username[256];
} af_packet_join_t;
#pragma pack()
/* version.c */
void af_common_version(unsigned char* v_major, unsigned char* v_minor, unsigned char* v_patch);
/* log.c */
void af_common_log_info(const char* fmt, ...);
#endif

View file

@ -1,6 +0,0 @@
#ifndef __AF_CONFIG_H__
#define __AF_CONFIG_H__
#define DATAROOTDIR "@CMAKE_INSTALL_FULL_DATAROOTDIR@"
#endif

View file

@ -1,8 +0,0 @@
#ifndef __AF_SERVER_H__
#define __AF_SERVER_H__
/* net.c */
void net_start(int port);
void net_loop(void);
#endif

View file

@ -1,13 +0,0 @@
#include <af_common.h>
#include <signal.h>
int main(int argc, char** argv) {
int port = 0;
signal(SIGPIPE, SIG_IGN);
net_start(port);
net_loop();
}

View file

@ -1,119 +0,0 @@
#include <af_common.h>
#include <stb_ds.h>
static fishsoup_server_t* server;
typedef struct opaque {
char username[256 + 1];
} opaque_t;
static void on_packet(fishsoup_client_t* self, fishsoup_packet_t* pkt) {
opaque_t* o = self->user;
if(pkt->header.type == AF_PACKET_JOIN && pkt->header.length == sizeof(af_packet_join_t)) {
af_packet_join_t* p_join = pkt->data;
int i;
o->username[256] = 0;
memcpy(o->username, p_join->username, 256);
af_common_log_info("Client %d: JOIN packet: Username \"%s\"\n", self->fd, o->username);
for(i = 0; i < arrlen(server->clients); i++) {
if(self->fd != server->clients[i].fd && strcmp(o->username, ((opaque_t*)server->clients[i].user)->username) == 0) {
break;
}
}
if(i == arrlen(server->clients)) {
fishsoup_packet_t p;
af_packet_ok_t p_ok;
p.header.type = AF_PACKET_OK;
p.header.length = sizeof(p_ok);
p.data = &p_ok;
memset(p_ok.reason, 0, 256);
fishsoup_packet_send(self->fd, &p);
af_common_log_info("Client %d: JOIN packet: Username is unused\n", self->fd);
} else {
fishsoup_packet_t p;
af_packet_error_t p_error;
p.header.type = AF_PACKET_ERROR;
p.header.length = sizeof(p_error);
p.data = &p_error;
memset(p_error.reason, 0, 256);
strcpy(p_error.reason, "Username is already used");
fishsoup_packet_send(self->fd, &p);
af_common_log_info("Client %d: JOIN packet: Username is already used\n", self->fd);
memset(o->username, 0, 256 + 1);
}
}
}
static void on_disconnect(fishsoup_client_t* self) {
free(self->user);
af_common_log_info("Client %d: Disconnected\n", self->fd);
}
static void on_client(fishsoup_server_t* self, fishsoup_client_t* client) {
opaque_t* o;
fishsoup_packet_t pkt;
af_packet_hello_t hello;
af_common_log_info("Client %d: Connected\n", client->fd);
o = client->user = malloc(sizeof(opaque_t));
o->username[0] = 0;
fishsoup_client_on_disconnect(client, on_disconnect);
fishsoup_client_on_packet(client, on_packet);
pkt.header.type = AF_PACKET_HELLO;
pkt.header.length = sizeof(hello);
pkt.data = &hello;
af_common_version(&hello.major, &hello.minor, &hello.patch);
fishsoup_packet_send(client->fd, &pkt);
}
static struct timespec c;
static void on_tick(fishsoup_server_t* self) {
struct timespec n;
unsigned long t1, t2, d;
clock_gettime(CLOCK_MONOTONIC, &n);
t1 = n.tv_sec * 1000 + n.tv_nsec / 1000000;
t2 = c.tv_sec * 1000 + c.tv_nsec / 1000000;
d = t1 - t2; /* elapsed time */
af_common_log_info("Tick: %dms\n", d);
c = n;
}
void net_start(int port) {
if(port == 0) port = AF_DEFAULT_PORT;
if((server = fishsoup_server(port)) == NULL) return;
fishsoup_server_on_client(server, on_client);
fishsoup_server_on_tick(server, on_tick);
}
void net_loop(void) {
if(server == NULL) return;
clock_gettime(CLOCK_MONOTONIC, &c);
fishsoup_server_loop(server);
}

View file

@ -1,14 +0,0 @@
#include <af_common.h>
static void af_common_log(const char* type, const char* fmt, va_list va) {
printf("[%.4s] ", type);
vprintf(fmt, va);
}
void af_common_log_info(const char* fmt, ...) {
va_list va;
va_start(va, fmt);
af_common_log("INFO", fmt, va);
va_end(va);
}

View file

@ -1,11 +0,0 @@
#include <af_common.h>
void af_common_version(unsigned char* v_major, unsigned char* v_minor, unsigned char* v_patch) {
char* mj = AF_VERSION;
char* mi = strchr(mj, '.') + 1;
char* pt = strchr(mi, '.') + 1;
*v_major = atoi(mj);
*v_minor = atoi(mi);
*v_patch = atoi(pt);
}