Giter VIP home page Giter VIP logo

Comments (21)

cursey avatar cursey commented on September 17, 2024 1

Try this:

// Refer to http://wiki.mabinogiworld.com/view/Lag#Lowering_your_Maximum_Transmission_Unit_.28MTU.29
// To change MTU on channel change/login
//  1. Set NET_INTERFACE to the correct interface name for your computer
//  2. Set LOW_MTU to the lowest value you're comfortable with, 386 is a good default
var NET_INTERFACE = "Ethernet"; //"Wi-Fi";
var LOW_MTU = 386;
var NORM_MTU = 1500;
/*
      1500 <768 < 512 < 386 < 256 < 128 < 48
  Slow<------------------------------------>Fast

  Win8.x/10
    Wired router:     netsh interface ipv4 set subinterface "Ethernet" mtu=386 store=persistent
    Wireless router:  netsh interface ipv4 set subinterface "Wi-Fi" mtu=386 store=persistent
  Win7/Vista
    Wired router:     netsh interface ipv4 set subinterface "Local Area Connection" mtu=386 store=persistent
    Wireless router:  netsh interface ipv4 set subinterface "Wireless Network Connection" mtu=386 store=persistent
*/

// Native functions used by this script.
var CreateProcessA = native('Kernel32.dll', 'CreateProcessA', 'int', ['pointer', 'pointer', 'pointer', 'pointer', 'int', 'ulong', 'pointer', 'pointer', 'pointer', 'pointer'], 'stdcall');
var WaitForSingleObject = native('Kernel32.dll', 'WaitForSingleObject', 'ulong', ['pointer', 'uint32'], 'stdcall');
var GetExitCodeProcess = native('Kernel32.dll', 'GetExitCodeProcess', 'int', ['pointer', 'pointer'], 'stdcall');
var CloseHandle = native('Kernel32.dll', 'CloseHandle', 'int', ['pointer'], 'stdcall');

// Constants used by this script.
var FALSE = 0;
var CREATE_NO_WINDOW = 0x08000000;
var NORMAL_PRIORITY_CLASS = 0x00000020;

// Returns -1 on failure, otherwise returns the exitcode of the process it 
// it created.
// 
// NOTE: If the process' exitcode is -1 then who knows if this function was 
// successful or not.
function runProcess(name, params) {
    //var namePtr = allocateStr(name);
    var paramsPtr = allocateStr(name + ' ' + params);
    var startupInfoPtr = allocateMemory(68);
    var processInfoPtr = allocateMemory(16);

    patchDword(startupInfoPtr, 68); // STARTUPINFO.cb = sizeof(STARTUPINFO) 

    var result = CreateProcessA(NULL, paramsPtr, NULL, NULL, FALSE, CREATE_NO_WINDOW | NORMAL_PRIORITY_CLASS, NULL, NULL, startupInfoPtr, processInfoPtr);

    dmsg("CreateProcess for " + name + " " + params + ": " + result);

    if (result != 0) {
         var processHandle = Memory.readPointer(processInfoPtr);
         var threadHandle = Memory.readPointer(processInfoPtr.add(4));
         var exitCodePtr = allocateMemory(4);

         result = WaitForSingleObject(processHandle, 5000);

         dmsg("WaitForSingleObject for " + processHandle + ": " + result);

         result = GetExitCodeProcess(processHandle, exitCodePtr);

         dmsg("GetExitCodeProcess for " + processHandle + ": " + result);

         result = Memory.readU32(exitCodePtr);

         dmsg("Exit code: " + Memory.readU32(exitCodePtr));

         freeMemory(exitCodePtr, 4);
         CloseHandle(threadHandle);
         CloseHandle(processHandle);
    }
    else {
        msg("Failed to start " + name + " with params " + params);
        result = -1;
    }

    freeMemory(processInfoPtr, 16);
    freeMemory(startupInfoPtr, 68);
    freeStr(paramsPtr);
    //freeStr(namePtr);

    return result;
}

// This function calls socket() and connect().
var createConnectionPtr = scan('55 8B EC 83 EC 08 56 57 8B 7D 0C 8B CF');

Interceptor.attach(createConnectionPtr, {
    onEnter(args) {
        // Lower MTU
        runProcess('netsh.exe', 'interface ipv4 set subinterface "' + NET_INTERFACE + '" mtu=' + LOW_MTU + ' store=persistent'); 
    },
    onLeave(retval) {
        // Raise MTU back to normal (1500)
        runProcess('netsh.exe', 'interface ipv4 set subinterface "' + NET_INTERFACE + '" mtu=' + NORM_MTU + ' store=persistent'); 
    }
});

Make sure to change NET_INTERFACE to whatever.

from kanan.

Aahzmandius avatar Aahzmandius commented on September 17, 2024 1

It works! 😁

Correctly changes MTU before mabi connection is made, and changes back after. Tested a bunch with the same scenarios, and even works after you close kanan (still not recommended to close, since a few other mods stop functioning). Thanks for the excellent work!

from kanan.

cursey avatar cursey commented on September 17, 2024

I've read over the documentation of Windows' ioctlsocket function and it seems quite limited compared to the normal Berkley socket's ioctl. The documentation even says to look at WSAIoctl for more options, which also does not seem to have any option for setting the MTU.

Unfortunately your examples seem to be for *nix systems where Berkley sockets are available. On Windows we are stuck with Winsock.

Might still be possible just not from ioctlsocket.

from kanan.

Aahzmandius avatar Aahzmandius commented on September 17, 2024

Yep, winsock appears useless. Even screwing around with WSAIoctl, I wasn't able to affect MTU or segment size at all. Also tried altering the socket's send buffer size (defaults to 65536, according to a getsockopt call, which is segmented at the network interface level according to MTU):

var IPPROTO_TCP = 6;
var TCP_NODELAY = 0x0001;
var SOL_SOCKET = 0xffff;
var SO_SNDBUF = 0x1001;

var socketCall = scan('E8 ? ? ? ? 8B F0 83 C4 0C 83 FE FF');
var socketOffset = Memory.readS32(socketCall.add(1));
var socketAddress = socketCall.add(5).toInt32() + socketOffset;

Interceptor.attach(ptr(socketAddress), {
    onLeave(retval) {
        // retval will be the result of the call to socket().
        var socket = retval.toInt32();
        var nodelay = allocateMemory(4);
        var lowersndbuf = allocateMemory(4);

        Memory.writeU32(nodelay, 1);
        Memory.writeU32(lowersndbuf, 386);
        dmsg("SNDBUF Set: "+ setsockopt(socket, SOL_SOCKET, SO_SNDBUF, lowersndbuf, 4));
        dmsg("WSAGetLastError: " + WSAGetLastError());
        dmsg("Send Buffer Size: "+Memory.readPointer(lowersndbuf).toInt32());

        dmsg("Socket: " + socket);
        dmsg("Setting TCP_NODELAY: " + setsockopt(socket, IPPROTO_TCP, TCP_NODELAY, nodelay, 4)); 
        dmsg("WSAGetLastError: " + WSAGetLastError());

        freeMemory(nodelay, 4);
        freeMemory(lowersndbuf, 4);
    }
})

Thought it would be effectively the same as lowering mtu, since it should be writing to less space and sending when that space is full, or something like that...? There wasn't much noticeable difference in any case, and running the .bat with netsh to change MTU between 1500 and 386 showed much better responsiveness than any of my testing. So winsock sucks, and that plan is dead in the water...

Wondering if I can get a call back to the python side and run a subprocess.call() to do netsh commands...? No idea if that would be fast enough to be set before the socket is initiated, and changed back afterwards of course.

from kanan.

cursey avatar cursey commented on September 17, 2024

The function that I intercept to set TCP_NODELAY is called in by a function that I think sets the buffer size. So your changed buffer size gets undone. I will check on this in a minute.

edit:
idaq_2016-09-11_07-42-14
Here you can see the highlighted socket call is the one I intercept. I was wrong in that it only sets the size of SO_RCVBUF, meaning your change to SO_SNDBUF was valid and it just didn't produce the results you wanted 😦

If there's no other way to do it besides running netsh we can either

  • Send a message to the python side of things, wait for a response while python runs netsh.
  • Use a win32 API from the JavaScript side of things to run netsh (CreateProcess or something similar).

I would prefer doing it from the JavaScript side of things since it doesn't create a dependency between a specific script and the python app.

from kanan.

Aahzmandius avatar Aahzmandius commented on September 17, 2024

Ahh right, could probably use ShellExecute from Shell32.dll. I'm unsure of the insertion points though, as it seems you'd need separate points before the socket is bound and after, to call the netsh commands. I'll test more after some sleep, if somebody doesn't beat me to it.

from kanan.

cursey avatar cursey commented on September 17, 2024

I've updated getProcAddress so that it will attempt to load the dll if it is not already loaded since I thought maybe Shell32.dll wasn't loaded by default (turns out it is but whatever).

from kanan.

Aahzmandius avatar Aahzmandius commented on September 17, 2024

Progress, but I'm getting a "successful error" of 42, of all things.

var setsockopt = new NativeFunction(getProcAddress('Ws2_32.dll', 'setsockopt'),
    'int', ['uint', 'int', 'int', 'pointer', 'int'], 'stdcall');
var WSAGetLastError = new NativeFunction(getProcAddress('Ws2_32.dll', 'WSAGetLastError'),
    'int', [], 'stdcall');
var getsockopt = new NativeFunction(getProcAddress('Ws2_32.dll', 'getsockopt'),
    'int', ['uint', 'int', 'int', 'pointer', 'pointer'], 'stdcall');
var shellexecute = new NativeFunction(getProcAddress('Shell32.dll', 'ShellExecuteA'),
    'int', ['pointer', 'pointer', 'pointer', 'pointer', 'pointer', 'int'], 'stdcall');

var IPPROTO_TCP = 6;
var TCP_NODELAY = 0x0001;

var socketCall = scan('E8 ? ? ? ? 8B F0 83 C4 0C 83 FE FF');
var socketOffset = Memory.readS32(socketCall.add(1));
var socketAddress = socketCall.add(5).toInt32() + socketOffset;

Interceptor.attach(ptr(socketAddress), {
    onEnter(args) {
        var netsh = allocateStr("netsh");
        var netshparams = allocateStr('interface ipv4 set subinterface "Wi-Fi" mtu=386 store persistent');
        var nulls = allocateMemory(4);
        dmsg("Setting mtu to 386: "+shellexecute(nulls, nulls, netsh, netshparams, nulls, 0));
        freeStr(netsh);
        freeStr(netshparams);
        freeMemory(nulls, 4);
    },
    onLeave(retval) {
        // retval will be the result of the call to socket().
        var socket = retval.toInt32();
        var nodelay = allocateMemory(4);

        Memory.writeU32(nodelay, 1);

        dmsg("Socket: " + socket);
        dmsg("Setting TCP_NODELAY: " + setsockopt(socket, IPPROTO_TCP, TCP_NODELAY, nodelay, 4)); 
        dmsg("WSAGetLastError: " + WSAGetLastError());

        freeMemory(nodelay, 4);
    }
});

The call to ShellExecuteA is fine and goes through correctly (returns greater than 32 = success according to docs). Unfortunately I think it's failing due to lack of admin privileges, which is probably because I'm passing in null for the window handle. Also note that I'm just trying to get the MTU down to 386 at this point, and once I can affect that, testing insertion timings and changing back to 1500 in onLeave() or whatever should be simple.

So I'm seeing no changes in an admin command prompt checking netsh interface ipv4 show subinterface "Wi-Fi", still 1500 after getting error 42...

Any ideas?

Edit: I've also tried the undocumented "runas" verb, still error 42.

from kanan.

cursey avatar cursey commented on September 17, 2024

Just a few things quickly,

  • You can get rid of var nulls and instead, pass NULL which is a frida NativePointer set to 0.
  • Instead of passing 0 (SW_HIDE) to the final param pass 5 (SW_SHOW) and see if it actually creates a console window that could maybe display the output of netsh (I've never actually modified my MTU).
  • You can get Mabi's HWND using FindWindowA (see BorderlessWindowedMode.js) if you think that it would make a difference.

I should also mention that since Mabi must be ran as admin, having it call ShellExecute probably runs it as admin as well already.

from kanan.

Aahzmandius avatar Aahzmandius commented on September 17, 2024

Ok, it's actually affecting MTU now, and I currently have it lowering in onEnter() and raising back to normal 1500 in onLeave(). Removing the part where it raises back to 1500, and testing by logging in and checking netsh interface ipv4 show subinterface "Wi-Fi" shows it correctly being changed to 386.

However, this doesn't seem to be early enough in the binding to affect the socket. I'm using a macro that spams lightning and ice bolt nonstop to test MTU difference, and there's still more of an effect when I manually do it with the .bat file than with my changes to kanan's DisableNagle.js. Here's my full current "working" version:

// Native functions used by this script.
var setsockopt = new NativeFunction(getProcAddress('Ws2_32.dll', 'setsockopt'),
    'int', ['uint', 'int', 'int', 'pointer', 'int'], 'stdcall');
var WSAGetLastError = new NativeFunction(getProcAddress('Ws2_32.dll', 'WSAGetLastError'),
    'int', [], 'stdcall');
var shellexecute = new NativeFunction(getProcAddress('Shell32.dll', 'ShellExecuteA'),
    'int', ['pointer', 'pointer', 'pointer', 'pointer', 'pointer', 'int'], 'stdcall');
var FindWindowA = new NativeFunction(getProcAddress('User32.dll', 'FindWindowA'),
    'pointer', ['pointer', 'pointer'], 'stdcall');

// Constants used by this script.
var IPPROTO_TCP = 6;
var TCP_NODELAY = 0x0001;

// Refer to http://wiki.mabinogiworld.com/view/Lag#Lowering_your_Maximum_Transmission_Unit_.28MTU.29
// To change MTU on channel change/login
//  1. Set MTU to true
//  2. Set NET_INTERFACE to the correct interface name for your computer
//  3. Set LOW_MTU to the lowest value you're comfortable with, 386 is a good default
var MTU = true;
var NET_INTERFACE = "Wi-Fi";
var LOW_MTU = 386;
var NORM_MTU = 1500;
/*
      1500 <768 < 512 < 386 < 256 < 128 < 48
  Slow<------------------------------------>Fast

  Win8.x/10
    Wired router:     netsh interface ipv4 set subinterface "Ethernet" mtu=386 store=persistent
    Wireless router:  netsh interface ipv4 set subinterface "Wi-Fi" mtu=386 store=persistent
  Win7/Vista
    Wired router:     netsh interface ipv4 set subinterface "Local Area Connection" mtu=386 store=persistent
    Wireless router:  netsh interface ipv4 set subinterface "Wireless Network Connection" mtu=386 store=persistent
*/

// This is actually not the call to socket(), but the call to the wrapper 
// function Mabi's packer has created to hide the import. Intercepting this just
// as if it was socket() is fine, but I got NGS kicked when intercepting the
// actual socket() function (could have been a fluke I didn't do further 
// testing).
var socketCall = scan('E8 ? ? ? ? 8B F0 83 C4 0C 83 FE FF');
var socketOffset = Memory.readS32(socketCall.add(1));
var socketAddress = socketCall.add(5).toInt32() + socketOffset;

Interceptor.attach(ptr(socketAddress), {
    onEnter(args) {
        if(MTU == true) {
            // Get the mabi window.
            var mabiStr = allocateStr('Mabinogi');
            var mabiWnd = FindWindowA(mabiStr, mabiStr);
            freeStr(mabiStr);

            // Lower MTU
            var netsh = allocateStr("netsh");
            var netshparams = allocateStr('interface ipv4 set subinterface "'+NET_INTERFACE+'" mtu='+LOW_MTU+' store=persistent');
            dmsg("Setting mtu to "+LOW_MTU+": "+shellexecute(mabiWnd, NULL, netsh, netshparams, NULL, 0));
            freeStr(netsh);
            freeStr(netshparams);
        }
    },
    onLeave(retval) {
        // retval will be the result of the call to socket().
        var socket = retval.toInt32();
        var nodelay = allocateMemory(4);

        Memory.writeU32(nodelay, 1);

        dmsg("Socket: " + socket);
        dmsg("Setting TCP_NODELAY: " + setsockopt(socket, IPPROTO_TCP, TCP_NODELAY, nodelay, 4)); 
        dmsg("WSAGetLastError: " + WSAGetLastError());

        freeMemory(nodelay, 4);

        if(MTU == true) {
            // Get the mabi window.
            var mabiStr = allocateStr('Mabinogi');
            var mabiWnd = FindWindowA(mabiStr, mabiStr);
            freeStr(mabiStr);

            // Raise MTU back to normal (1500)
            var netsh = allocateStr("netsh");
            var netshparams = allocateStr('interface ipv4 set subinterface "'+NET_INTERFACE+'" mtu='+NORM_MTU+' store=persistent');
            dmsg("Setting mtu to "+NORM_MTU+": "+shellexecute(mabiWnd, NULL, netsh, netshparams, NULL, 0));
            freeStr(netsh);
            freeStr(netshparams);
        }

    }
});

So close to being functional!

Is there a safe, earlier point to intercept the channel changing?

from kanan.

cursey avatar cursey commented on September 17, 2024

When you remove the reset code from onLeave do you see the expected difference? As in, does your spamming speed increase if you dont reset the MTU to 1500?

from kanan.

Aahzmandius avatar Aahzmandius commented on September 17, 2024

Yes, removing the reset code allowed fast skill spam, but it basically doesn't count because the TCP_NODELAY and this MTU change happen multiple times before you're even in-game:

  1. Once after entering account info (set from 1500 MTU to 386)
  2. Once after correctly entering secondary password (set from 386 MTU to 386)
  3. A final time when logging into a channel with a character (set from 386 MTU to 386)

With it changing the MTU to 386 and not changing it back after the first count, of course it will still be 386 by the time you're in-game and can spam skills. I did test it this way however, and before the final (3rd) step logging into a channel with a character, I did an admin command prompt and ran netsh interface ipv4 set subinterface "Wi-Fi" mtu=1500 store=persistent again, to make sure it was reset to the default before the MTU code ran. Doing it that way my skills were still slow, behaving at the rate of 1500 MTU, even though I double checked in an admin prompt that the MTU was indeed set to 386 correctly by kanan.

So, this isn't early enough to affect the socket for MTU changing unfortunately, though it does seem to work for TCP_NODELAY.

from kanan.

cursey avatar cursey commented on September 17, 2024

Yeah, so it sounds like MTU doesn't take effect until the socket is bound to a connection (eg, just creating it isn't enough to properly set the MTU). I can fix this most likely by hooking the function that calls socket since it also calls connect. This will let us separate the MTU script from the DisableNagle.js script.

I'll use most of what you've written and would like you to test it once I put it here if that's okay.

from kanan.

Aahzmandius avatar Aahzmandius commented on September 17, 2024

Sounds great, will test whenever it's up, take your time 😄

from kanan.

cursey avatar cursey commented on September 17, 2024

Alright here it is completely 100% untested. Let me know how it goes (fingers crossed).

// Refer to http://wiki.mabinogiworld.com/view/Lag#Lowering_your_Maximum_Transmission_Unit_.28MTU.29
// To change MTU on channel change/login
//  1. Set NET_INTERFACE to the correct interface name for your computer
//  2. Set LOW_MTU to the lowest value you're comfortable with, 386 is a good default
var NET_INTERFACE = "Wi-Fi";
var LOW_MTU = 386;
var NORM_MTU = 1500;
/*
      1500 <768 < 512 < 386 < 256 < 128 < 48
  Slow<------------------------------------>Fast

  Win8.x/10
    Wired router:     netsh interface ipv4 set subinterface "Ethernet" mtu=386 store=persistent
    Wireless router:  netsh interface ipv4 set subinterface "Wi-Fi" mtu=386 store=persistent
  Win7/Vista
    Wired router:     netsh interface ipv4 set subinterface "Local Area Connection" mtu=386 store=persistent
    Wireless router:  netsh interface ipv4 set subinterface "Wireless Network Connection" mtu=386 store=persistent
*/



// Native functions used by this script.
var ShellExecuteA = native('Shell32.dll', 'ShellExecuteA', 'int', ['pointer', 'pointer', 'pointer', 'pointer', 'pointer', 'int'], 'stdcall');
var FindWindowA = native('User32.dll', 'FindWindowA', 'pointer', ['pointer', 'pointer'], 'stdcall');

// Constants used by this script.
var SW_HIDE = 0;

// Get the mabi window.
var mabiStr = allocateStr('Mabinogi');
var mabiWnd = FindWindowA(mabiStr, mabiStr);

freeStr(mabiStr);

// This function calls socket() and connect().
var createConnectionPtr = scan('55 8B EC 83 EC 08 56 57 8B 7D 0C 8B CF');

Interceptor.attach(createConnectionPtr, {
    onEnter(args) {
        // Lower MTU
        var netsh = allocateStr("netsh");
        var netshparams = allocateStr('interface ipv4 set subinterface "' + NET_INTERFACE + '" mtu=' + LOW_MTU + ' store=persistent');

        dmsg("Setting mtu to " + LOW_MTU + ": " + ShellExecuteA(mabiWnd, NULL, netsh, netshparams, NULL, 0));

        freeStr(netsh);
        freeStr(netshparams);
    },
    onLeave(retval) {
        // Raise MTU back to normal (1500)
        var netsh = allocateStr("netsh");
        var netshparams = allocateStr('interface ipv4 set subinterface "' + NET_INTERFACE + '" mtu=' + NORM_MTU + ' store=persistent');

        dmsg("Setting mtu to " + NORM_MTU + ": " + ShellExecuteA(mabiWnd, NULL, netsh, netshparams, NULL, 0));
        freeStr(netsh);
        freeStr(netshparams);
    }
});

This will be called AutoSetMTU.js if/when it works. Oh, I should mention you need to update to the latest Defaults.js for you to have the native() helper.

from kanan.

cursey avatar cursey commented on September 17, 2024

It seems to be working, but I live right near the server so I can't test to see if it's making me faster.
cmd_2016-09-12_03-02-36

from kanan.

Aahzmandius avatar Aahzmandius commented on September 17, 2024

Still testing, will update when I know for sure.

Update: The reset back to 1500 is working perfectly. But the lowering MTU part isn't early enough it seems, did extensive testing a dozen times on each scenario. Managed to capture some gifs that just about show the difference.

Normal with Kanan handling MTU
The first is just letting kanan do its thing with the new script coalesced, debug window looks exactly the same as yours. It's still as slow as 1500 MTU sadly, and in the gif you can see how choppy it is, and notice how much more I lean down.

Kanan handling MTU, but primed to 386 MTU manually
The second is after I do a manual change to 386 MTU in an admin command prompt window, before I change channels to the same channel, and repeat the test while recording. This time the socket binds with the lower MTU, and skills spam faster. Notice I don't lean down as much, and the skill bubbles are much faster.
Spam macro
My macro has no delay between keypresses and uses an on/off toggle on my mouse to continuously spam. Both gifs are recorded with the same settings, 30 FPS, in the exact same location, exact same camera angle, exact same channel. There's no sound in the gifs, but I can also hear the difference.

Hmm, how much sooner can you go with the connection pointer? 😀

from kanan.

cursey avatar cursey commented on September 17, 2024

I'm really bummed out to hear that it's not working from there. I mean, sockets don't really start until socket() is called and that is the function that calls socket(). It doesn't really get earlier than that.

I looked anyway and unfortunately, that function is being called from a function that has been mutated by Nexon's protection packer. Meaning the instructions are jumbled and hard to follow.

I'll try to think of something.

I think your gifs are pretty definitive but could you maybe try setting it to something ridiculous like greater than 1500. If it was working properly you should suffer from massive packet loss and things should be a lot slower.

from kanan.

Aahzmandius avatar Aahzmandius commented on September 17, 2024

Hmm, well as an exit point to reset the connection back to 1500, it works great. For an entry point, it really doesn't even have to be where the socket is connecting, since we're not trying to affect direct settings like TCP_NODELAY. Just so long as it's tied into something that's related to changing channels or logging in.

And yes, manually setting it to 4500 and changing channels causes it to use the 4500 MTU and set connection back down to 1500 when it hits that onLeave(). So when I check netsh interface ipv4 show subinterface "Wi-Fi", it displays 1500, but in-game I'm actually lagged to death with 4500.

from kanan.

cursey avatar cursey commented on September 17, 2024

I'm wondering if ShellExecute returns before netsh has been ran and closed. That would make sense as to why this doesn't work.

from kanan.

KevinSkull avatar KevinSkull commented on September 17, 2024

Is it normal for MTU to allow for fast spam skill, that allows for abuse no? sorry I don't really understand the very technical aspects you talked above about, but the reason for the ability to spam skill before the cooldown is done is because of the client taking a shorter time to go to the server to communicate, but shouldn't that stop since it is being on cooldown? or is it since mabinogi was designed to go slower with the communication? or it just depends on how close/far you are from the server to be enable to skill spam?

from kanan.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.