/* _______________ ,----------------------------------------------------------------. //`````````````\\ \ \ //~~~~~~~~~~~~~~~\\ \ by @ohsqueezy & @sleepin \ //=================\\ \ [ohsqueezy.itch.io] [sleepin.itch.io] \ // \\ \ \ // \\ \ code released under the zlib license [git.nugget.fun/pudding] \ // ☆ GUNKISS ☆ \\ \ \ //_________________________\\ `---------------------------------------------------------------*/ #ifndef PUDDING_H_ #define PUDDING_H_ /* Needed for functions in glm/gtx/ */ #define GLM_ENABLE_EXPERIMENTAL /* cURL and cv::VideoCapture are not available for Emscripten, so use alternatives for Emscripten builds */ #if defined(__EMSCRIPTEN__) #include #include using namespace emscripten; #else #include #include "opencv2/videoio.hpp" #include "opencv2/highgui.hpp" #endif #include #include #include #include #include #include #include #include #include #include #include "SDL.h" #include "SDL_image.h" #include "sdl2-gfx/SDL2_gfxPrimitives.h" #include "json/json.hpp" #include "glm/glm.hpp" #include "glm/gtx/matrix_decompose.hpp" #include "opencv2/core.hpp" #include "opencv2/imgproc.hpp" #include "opencv2/barcode.hpp" #include "zbar.h" #include "Game.hpp" #include "Color.hpp" #include "extension.hpp" #include "filesystem.hpp" #include "Animation.hpp" #include "Texture.hpp" #include "GLObject.hpp" #include "Log.hpp" #include "Attributes.hpp" #include "VBO.hpp" #include "Item.hpp" #include "Model.hpp" #include "utility.hpp" #include "Box.hpp" /*! * This class is used for printing the running time of a function or lambda. It can be * inserted inline into existing code by wrapping the block to time in a lambda. It can also be * given a function and optionally the function's arguments and will return a value of the type specified. * * The following code * * ~~~{.cpp} * std::string fake(std::string x, std::string y) * { * return x + y; * } * * // Needs SPACEBOX initialization for log call * int ii = 0; * time_it()([&] { while (ii++ < 1000); }); * ii = 0; * int revolving = time_it("for revolving")([&](int x) -> int { while (ii++ < 10000); return x; }, 1); * ii = 0; * std::cout << "Revolving is " << revolving << std::endl; * std::string addition = time_it("for combination")(fake, "Hello, ", "World!"); * std::cout << "Combination is " << addition << std::endl; * time_it("for many combinations")([&] { while (ii++ < 100000) { fake("a", "b"); } }); * ~~~ * * gives the expected output * * Elapsed time: 2.588e-06s * Elapsed time for revolving: 2.1547e-05s * Revolving is 1 * Elapsed time for combination: 7.27e-07s * Combination is Hello, World! * Elapsed time for many combinations: 0.0107351s */ template class time_it { private: std::string info = ""; public: time_it(std::string info = "") : info(info) {}; return_type operator()(const std::function&& block, arguments... args) { std::cout << "{Elapsed time"; if (!info.empty()) { std::cout << " " << info; } std::cout << ": "; auto start = std::chrono::steady_clock::now(); std::chrono::duration elapsed; if constexpr (std::is_same_v) { block(args...); elapsed = std::chrono::steady_clock::now() - start; std::cout << elapsed.count() << "s}" << std::endl; } else { return_type result = block(args...); elapsed = std::chrono::steady_clock::now() - start; std::cout << elapsed.count() << "s}" << std::endl; return result; } } }; /* A connection is an object containing a binary state of either on (connected) or off (not connected) * and user supplied functions that run automatically on each state change. The functions each have the * same return type, number of arguments, and argument types determined by the template arguments. * * Original test code: * * Connection<> connection_d(std::bind(&Game::print_frame_length_history, this)); * connection_d.toggle(); * Connection connection_f { * std::function(&sb::mod), std::function(&sb::mod) }; * Connection<> connection_g = connection_d; * connection_g.toggle(); * connection_g.disconnect(); * int result; * result = connection_f.connect(3, 5); * std::cout << result << " "; * std::cout << connection_f.disconnect(20, 6) << " "; * result = connection_f.toggle(800, 120); * std::cout << result << std::endl; * result = connection_f.connect(111, 44); * std::cout << result << std::endl; */ template class Connection { private: enum State : bool { STATE_OFF, STATE_ON }; using callback = std::function; State connection_state = STATE_OFF; callback on_connect_callback, on_disconnect_callback; public: /* Without any arguments, the connection object will be in the disconnected state with empty functions. Otherwise, * the supplied functions will be added to the connection object. The first function argument will be run on a * connection, and the second function argument will be run on a disconnection. */ Connection(callback on_connect_callback = callback(), callback on_disconnect_callback = callback()) { if (on_connect_callback) { on_connect(on_connect_callback); if (on_disconnect_callback) { on_disconnect(on_disconnect_callback); } } } /* Set the function that will run when a connection is made. */ void on_connect(callback on_connect) { on_connect_callback = on_connect; } /* Set the function that will run when a disconnection happens. */ void on_disconnect(callback on_disconnect) { on_disconnect_callback = on_disconnect; } /* Set state to Connection::STATE_ON and run response function. If return_type is non-void and the * connection is already connected, the function will not run and the return value will be a default * constructed value of the type return_type. Therefore, return_type must be default constructible. */ return_type connect(arguments... args) { if (!*this) { connection_state = STATE_ON; if (on_connect_callback) { return on_connect_callback(args...); } } return return_type(); } /* Set state to Connection::STATE_OFF and run response function. If return_type is non-void and the * connection is already disconnected, the function will not run and the return value will be a default * constructed value of the type return_type. Therefore, return_type must be default constructible. */ return_type disconnect(arguments... args) { if (*this) { connection_state = STATE_OFF; if (on_disconnect_callback) { return on_disconnect_callback(args...); } } return return_type(); } /* Set state to the opposite of current state, causing the appropriate response function to run. */ return_type toggle(arguments... args) { if (*this) { return disconnect(args...); } else { return connect(args...); } } /* Return true if state is Connection::STATE_ON, false otherwise. */ bool connected() { return connection_state; } /* When called as a boolean, return the connection state. */ operator bool() { return connected(); } }; /* Drawable class that is a plane containing a connection. Each instance: * * - Shares vertices and UV in VBO * - Has its own Texture representing the button on-screen * - Has its own response to click * - Shares mouse collision code * - Has its own translate + scale transformation * * Example: * * glm::vec3 w = glm::mat3({{1, 0, 0}, {0, 1, 0}, {-0.6739, -0.74, 1}}) * glm::mat3({{.1, 0, 0}, {0, .1 * (460.0 / 768.0), 0}, {0, 0, 1}}) * * glm::vec3({-1, -1, 1}); * std::cout << w << std::endl << glm::translate(glm::vec3{-0.6739, -0.74, 0}) * * glm::scale(glm::vec3{.1, .1 * (460.0 / 768.0), 1}) * glm::vec4{-1, -1, 0, 1} << std::endl; * Pad p {background.current(), {-0.6739f, -0.74f}, 0.1f, get_display().window_box().aspect(), std::function()}; * const std::vector& p_position = *p.attributes("position"); * glm::vec4 final_position = p.transformation() * glm::vec4{p_position[2].x, p_position[2].y, 0, 1}; * std::cout << p.transformation() << std::endl << final_position << std::endl; * assert(final_position == glm::vec4({w.x, w.y, 0, 1})); */ class Pad : public Plane { private: inline static const glm::vec3 ROTATION_AXIS {0.0f, 0.0f, 1.0f}; using callback = std::function; Connection<> connection; Box collision_box; float rotation_angle = 0.0f, scale_factor = 0.0f, scale_ratio = 1.0f; glm::vec2 translation_vector {0.0f, 0.0f}; void transform(); public: Pad() {}; Pad(sb::Texture, glm::vec2, float, float, callback, float = 0.0f); void rotation(float); void scale(float, float = 1.0f); void translation(const glm::vec2&); void on_connect(callback); bool collide(const glm::vec2&) const; void draw(GLuint); }; /* These variables will be bound to JS. They are placed in the global scope, so they can be read and written by both * C++ and JS. The associated functions are bound to JS so they can be used to write values to the variables. The * new_frame_available flag is used by both C++ and JS builds, so it is always included. */ bool new_frame_available = false; #ifdef __EMSCRIPTEN__ int emscripten_heap_offset = -1; void flag_frame(); void set_heap_offset(int offset); #endif /*! * Type declaration of a function that will accept a vector of bytes for the response data from a web request and a string for the URL. */ using web_callback = std::function&, const std::string&)>; /*! * Store the state, response data, and response function of a request for web data sent to either cURL or Emscripten. */ class Request { private: web_callback callback = nullptr; std::vector response; bool is_finished = false; std::string request_url; public: /*! * Construct a request object, specifying a callback that will be passed the complete data in bytes. The callback must therefore accept a * vector of bytes. The URL of the request can be stored. * * @param callback A function object that accepts a vector of bytes. * @param url URL of the request */ Request(const web_callback& callback, const std::string& url = ""); /*! * Get the URL of the request if it has been specified. * * @return The URL of the request or an empty string if the URL was not set */ const std::string& url() const; /*! * Add the bytes pointed to by buffer to the storage vector. */ void store(const std::uint8_t* buffer, const std::size_t& size); /*! * Call the user supplied callback and set state to finished. */ void respond(); /*! * Set the finished state to true. */ void mark_finished(); /*! * Check if the request is complete, meaning the data has been stored in memory and the callback has run. * * @return true if complete, false otherwise */ const bool& finished() const; }; /*! * The main game object. There is currently only support for one of these to exist at a time. */ class Pudding : public Game { private: /* Defines for effect IDs that will be passed to the shader program. Since EFFECT_COUNT is last and every value * is the default integer, it will be set to the number of effects available. */ enum Effect { EFFECT_NONE, EFFECT_SNAKE, EFFECT_WOBBLE, EFFECT_COUNT }; /* Defines for UV transformations available in the fragment shader program */ enum UVTransformation { UV_NONE, UV_SQUIRCLE }; /* Convention for calling parent class in a consistent way across classes */ typedef Game super; /* Constants */ inline static const std::string OPEN_FOOD_API_URL = "https://world.openfoodfacts.org/api/v0/product/"; inline static const std::string OPEN_PRODUCTS_API_URL = "https://world.openproductsfacts.org/api/v0/product/"; inline static const std::string NUTRITIONIX_API_URL = "https://trackapi.nutritionix.com/v2/search/item?upc="; inline static const std::string BARCODE_MONSTER_API_URL = "https://barcode.monster/api/"; inline static const std::string BEST_BUY_API_URL_1 = "https://api.bestbuy.com/v1/products(upc="; inline static const std::string BEST_BUY_API_URL_2 = ")?format=json&apiKey="; inline static const std::string NUTRITIONIX_NOT_FOUND = "resource not found"; inline static const std::string GOOGLE_BOOKS_API_URL = "https://www.googleapis.com/books/v1/volumes?q=isbn:"; inline static const std::string GIANTBOMB_API_URL = "https://www.giantbomb.com/api/release/?api_key="; inline static const std::string CORS_ANYWHERE_PROXY_URL = "https://mario.shampoo.ooo:8088/"; inline static const glm::vec3 ZERO_VECTOR_3D {0, 0, 0}; inline static const glm::vec3 Y_UNIT_NORMAL_3D {0, 1, 0}; inline static const glm::mat4 VIEW_MATRIX = glm::lookAt({4.0f, 2.0f, 1.0f}, {0.0f, -0.325f, 0.0f}, Y_UNIT_NORMAL_3D); inline static const glm::vec3 PUDDING_BROWN {0.713f, 0.359f, 0.224f}; inline static const glm::vec3 PUDDING_YELLOW {0.878f, 0.859f, 0.122f}; /* Member variables */ std::shared_ptr poke; std::string current_barcode, previous_barcode, current_config_barcode; std::vector items; Item incoming_item; Carousel item_carousel; int effect_id = EFFECT_NONE, pudding_triangle_vertex_count = 0, pudding_fan_vertex_count = 0; #ifndef __EMSCRIPTEN__ cv::VideoCapture capture; #endif cv::Mat camera_frame, contrasted_frame, sharpened_frame, zbar_frame, blurred, low_contrast_mask; zbar::ImageScanner image_scanner; std::map> uniform; GLuint flat_program, mvp_program; glm::mat4 projection, model {1.0f}, mvp; Model pudding_model; Plane plane, camera_view; Background background; bool show_item = false; sb::VAO vao; sb::VBO vbo; std::map labels; Pad camera_button, previous_button, next_button, inventory_button; Box viewport, main_viewport, pop_up_viewport; std::vector requests; /* Storage for barcode scanning module */ cv::Ptr barcode_detector = cv::makePtr(); std::vector barcode_info; std::vector barcode_type; std::vector barcode_corners; void load_pudding_model(float, float, int, int = 1, float = -1.0f, float = 1.0f, float = 0.3f); void load_gl_context(); void load_tiles(); void load_pads(); /*! * Try to create cv::VideoCapture object using device ID #0. If successful, this will also create a GL texture ID and * storage for the camera frame on the GPU, so it must be called after GL context has been created. Create and detach * a thread which will continuously read frame data. */ void open_camera(); /*! * Release camera resources. */ void close_camera(); /*! * Check the response from Open Food/Products API and use the result to fill out item properties if found. Request the image * data if an image URL is found. * * @param storage JSON as raw bytes fetched from the web, written to a vector * @param url URL of the request */ void incorporate_open_api(const std::vector& json_bytes, const std::string& url); /*! * Check the response from Nutritionix API and use the result to fill out item properties if found. Request the image data if an * image URL is found. * * @param storage JSON as raw bytes fetched from the web, written to a vector * @param url URL of the request */ void incorporate_nutritionix_api(const std::vector& json_bytes, const std::string& url); /*! * Check the response from Edamame API and use the result to fill out item properties if found. Request the image data if an * image URL is found. * * @param storage JSON as raw bytes fetched from the web, written to a vector * @param url URL of the request */ void incorporate_edamam_api(const std::vector& json_bytes, const std::string& url); /*! * Check the response from Best Buy API and use the result to fill out item properties if found. Request image data if an * image URL is found. * * @param storage JSON as raw bytes fetched from the web, written to a vector * @param url URL of the request */ void incorporate_best_buy_api(const std::vector& json_bytes, const std::string& url); /*! * Check the response from Google API and use the result to fill out item properties if found. Request image data if an * image URL is found. * * @param storage JSON as raw bytes fetched from the web, written to a vector * @param url URL of the request */ void incorporate_google_books_api(const std::vector& json_bytes, const std::string& url); void save_item_json(const nlohmann::json&, const Item&, const std::string&) const; /*! * Fetch data from `url` as raw bytes. The data will be copied into a vector which will be passed to a user supplied function. A request object * will be added to `launched_requests`. That vector can be checked to determine when all requests are complete. * * The compiler will determine whether to use cURL or the Emscripten Fetch API to do the retrieval, depending on whether it is compiling for * Emscripten. * * @param url URL containing data to be retrieved * @param callback A function pointer for a function that accepts a reference to a vector of bytes and a reference to an Item. The bytes are the * response data retrieved from `url`. The function can be any arbitrary code that uses the data. * @param headers Request headers as a vector of strings formatted as ["name1", "value1", "name2", "value2", ...] */ void web_get_bytes(std::string url, const web_callback& callback, const std::vector& headers = {}); static void destroy_texture(GLuint*); bool item_display_active() const; void capture_frame(); /*! * Create a texture to store the image data and add it to the incoming item object. * * @param image image data as raw bytes fetched from the web, written to a vector * @param url image URL which will will be the Texture's name */ void store_web_image(const std::vector& image, const std::string& url); /* Declare the appropriate callbacks for asynchronous web data loaders. Either cURL by default, or Fetch if compiling for Emscripten. */ #if defined(__EMSCRIPTEN__) /*! * This will be called automatically when request data is sucessfully fetched by `emscripten_fetch` from `Pudding::web_get_bytes`. * Data will be written to a vector, and a user supplied callback will be called and passed the data. The callback requested by the * caller is in fetch->userData. * * @param fetch an object created by Emscripten that stores parameters for accessing the downloaded data */ static void fetch_success(emscripten_fetch_t* fetch); /*! * This will be called automatically when request data is not successfully fetched by `emscripten_fetch` from `Pudding::web_get_bytes`. * * @param fetch an object created by Emscripten that stores parameters related to the request */ static void fetch_error(emscripten_fetch_t* fetch); #else /*! * This will be called by cURL when it has received a buffer of data. The data will be stored by the object at `request`. This may * be called multiple times before the entire data received from the originally submitted URL is received. * * @param buffer pointer to data cURL is transferring into memory * @param size size in bytes of each value * @param count number of values * @param request pointer to a request object which will store the data which will be freed in the update loop * @return number of bytes copied */ static std::size_t curl_write_response(std::uint8_t* buffer, std::size_t size, std::size_t count, Request* request); #endif /* Open camera on connection and close on disconnection. */ Connection<> camera_switch { std::bind(&Pudding::open_camera, this), std::bind(&Pudding::close_camera, this) }; public: Pudding(); void respond(SDL_Event&); void add_item(const std::string&); Item& current_item(); void update(); virtual std::string class_name() const { return "Pudding"; } }; /* Apply force until reaching a threshold. Use a connection object to run user functions * when force reaches threshold and when force goes below threshold. */ template class Button { private: Connection connection; /* float threshold = 1.0f */ /* float force = 0.0f */ /* apply() */ /* remove() */ /* float weighted depression rate */ }; /* Allow a box object to be passed to glViewport instead of four vertices. */ void glViewport(Box); #endif